1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.printspooler.ui;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.os.ParcelFileDescriptor;
27 import android.print.PageRange;
28 import android.print.PrintAttributes.Margins;
29 import android.print.PrintAttributes.MediaSize;
30 import android.print.PrintDocumentInfo;
31 import android.support.v7.widget.RecyclerView.Adapter;
32 import android.support.v7.widget.RecyclerView.ViewHolder;
33 import android.util.Log;
34 import android.util.SparseArray;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.View.MeasureSpec;
38 import android.view.View.OnClickListener;
39 import android.view.ViewGroup;
40 import android.view.ViewGroup.LayoutParams;
41 import android.widget.TextView;
42 
43 import com.android.printspooler.R;
44 import com.android.printspooler.model.OpenDocumentCallback;
45 import com.android.printspooler.model.PageContentRepository;
46 import com.android.printspooler.model.PageContentRepository.PageContentProvider;
47 import com.android.printspooler.util.PageRangeUtils;
48 import com.android.printspooler.widget.PageContentView;
49 import com.android.printspooler.widget.PreviewPageFrame;
50 
51 import dalvik.system.CloseGuard;
52 
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.List;
56 
57 /**
58  * This class represents the adapter for the pages in the print preview list.
59  */
60 public final class PageAdapter extends Adapter<ViewHolder> {
61     private static final String LOG_TAG = "PageAdapter";
62 
63     private static final int MAX_PREVIEW_PAGES_BATCH = 50;
64 
65     private static final boolean DEBUG = false;
66 
67     private static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {
68             PageRange.ALL_PAGES
69     };
70 
71     private static final int INVALID_PAGE_INDEX = -1;
72 
73     private static final int STATE_CLOSED = 0;
74     private static final int STATE_OPENED = 1;
75     private static final int STATE_DESTROYED = 2;
76 
77     private final CloseGuard mCloseGuard = CloseGuard.get();
78 
79     private final SparseArray<Void> mBoundPagesInAdapter = new SparseArray<>();
80     private final SparseArray<Void> mConfirmedPagesInDocument = new SparseArray<>();
81 
82     private final PageClickListener mPageClickListener = new PageClickListener();
83 
84     private final Context mContext;
85     private final LayoutInflater mLayoutInflater;
86 
87     private final ContentCallbacks mCallbacks;
88     private final PageContentRepository mPageContentRepository;
89     private final PreviewArea mPreviewArea;
90 
91     // Which document pages to be written.
92     private PageRange[] mRequestedPages;
93     // Pages written in the current file.
94     private PageRange[] mWrittenPages;
95     // Pages the user selected in the UI.
96     private PageRange[] mSelectedPages;
97 
98     private BitmapDrawable mEmptyState;
99     private BitmapDrawable mErrorState;
100 
101     private int mDocumentPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
102     private int mSelectedPageCount;
103 
104     private int mPreviewPageMargin;
105     private int mPreviewPageMinWidth;
106     private int mPreviewListPadding;
107     private int mFooterHeight;
108 
109     private int mColumnCount;
110 
111     private MediaSize mMediaSize;
112     private Margins mMinMargins;
113 
114     private int mState;
115 
116     private int mPageContentWidth;
117     private int mPageContentHeight;
118 
119     public interface ContentCallbacks {
onRequestContentUpdate()120         public void onRequestContentUpdate();
onMalformedPdfFile()121         public void onMalformedPdfFile();
onSecurePdfFile()122         public void onSecurePdfFile();
123     }
124 
125     public interface PreviewArea {
getWidth()126         public int getWidth();
getHeight()127         public int getHeight();
setColumnCount(int columnCount)128         public void setColumnCount(int columnCount);
setPadding(int left, int top, int right, int bottom)129         public void setPadding(int left, int top, int right, int bottom);
130     }
131 
PageAdapter(Context context, ContentCallbacks callbacks, PreviewArea previewArea)132     public PageAdapter(Context context, ContentCallbacks callbacks, PreviewArea previewArea) {
133         mContext = context;
134         mCallbacks = callbacks;
135         mLayoutInflater = (LayoutInflater) context.getSystemService(
136                 Context.LAYOUT_INFLATER_SERVICE);
137         mPageContentRepository = new PageContentRepository(context);
138 
139         mPreviewPageMargin = mContext.getResources().getDimensionPixelSize(
140                 R.dimen.preview_page_margin);
141 
142         mPreviewPageMinWidth = mContext.getResources().getDimensionPixelSize(
143                 R.dimen.preview_page_min_width);
144 
145         mPreviewListPadding = mContext.getResources().getDimensionPixelSize(
146                 R.dimen.preview_list_padding);
147 
148         mColumnCount = mContext.getResources().getInteger(
149                 R.integer.preview_page_per_row_count);
150 
151         mFooterHeight = mContext.getResources().getDimensionPixelSize(
152                 R.dimen.preview_page_footer_height);
153 
154         mPreviewArea = previewArea;
155 
156         mCloseGuard.open("destroy");
157 
158         setHasStableIds(true);
159 
160         mState = STATE_CLOSED;
161         if (DEBUG) {
162             Log.i(LOG_TAG, "STATE_CLOSED");
163         }
164     }
165 
onOrientationChanged()166     public void onOrientationChanged() {
167         mColumnCount = mContext.getResources().getInteger(
168                 R.integer.preview_page_per_row_count);
169         notifyDataSetChanged();
170     }
171 
isOpened()172     public boolean isOpened() {
173         return mState == STATE_OPENED;
174     }
175 
getFilePageCount()176     public int getFilePageCount() {
177         return mPageContentRepository.getFilePageCount();
178     }
179 
open(ParcelFileDescriptor source, final Runnable callback)180     public void open(ParcelFileDescriptor source, final Runnable callback) {
181         throwIfNotClosed();
182         mState = STATE_OPENED;
183         if (DEBUG) {
184             Log.i(LOG_TAG, "STATE_OPENED");
185         }
186         mPageContentRepository.open(source, new OpenDocumentCallback() {
187             @Override
188             public void onSuccess() {
189                 notifyDataSetChanged();
190                 callback.run();
191             }
192 
193             @Override
194             public void onFailure(int error) {
195                 switch (error) {
196                     case OpenDocumentCallback.ERROR_MALFORMED_PDF_FILE: {
197                         mCallbacks.onMalformedPdfFile();
198                     } break;
199 
200                     case OpenDocumentCallback.ERROR_SECURE_PDF_FILE: {
201                         mCallbacks.onSecurePdfFile();
202                     } break;
203                 }
204             }
205         });
206     }
207 
update(PageRange[] writtenPages, PageRange[] selectedPages, int documentPageCount, MediaSize mediaSize, Margins minMargins)208     public void update(PageRange[] writtenPages, PageRange[] selectedPages,
209             int documentPageCount, MediaSize mediaSize, Margins minMargins) {
210         boolean documentChanged = false;
211         boolean updatePreviewAreaAndPageSize = false;
212         boolean clearSelectedPages = false;
213 
214         // If the app does not tell how many pages are in the document we cannot
215         // optimize and ask for all pages whose count we get from the renderer.
216         if (documentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
217             if (writtenPages == null) {
218                 // If we already requested all pages, just wait.
219                 if (!Arrays.equals(ALL_PAGES_ARRAY, mRequestedPages)) {
220                     mRequestedPages = ALL_PAGES_ARRAY;
221                     mCallbacks.onRequestContentUpdate();
222                 }
223                 return;
224             } else {
225                 documentPageCount = mPageContentRepository.getFilePageCount();
226                 if (documentPageCount <= 0) {
227                     return;
228                 }
229             }
230         }
231 
232         if (mDocumentPageCount != documentPageCount) {
233             mDocumentPageCount = documentPageCount;
234             documentChanged = true;
235             clearSelectedPages = true;
236         }
237 
238         if (mMediaSize == null || !mMediaSize.equals(mediaSize)) {
239             mMediaSize = mediaSize;
240             updatePreviewAreaAndPageSize = true;
241             documentChanged = true;
242 
243             clearSelectedPages = true;
244         }
245 
246         if (mMinMargins == null || !mMinMargins.equals(minMargins)) {
247             mMinMargins = minMargins;
248             updatePreviewAreaAndPageSize = true;
249             documentChanged = true;
250 
251             clearSelectedPages = true;
252         }
253 
254         if (clearSelectedPages) {
255             mSelectedPages = PageRange.ALL_PAGES_ARRAY;
256             mSelectedPageCount = documentPageCount;
257             setConfirmedPages(mSelectedPages, documentPageCount);
258             updatePreviewAreaAndPageSize = true;
259             documentChanged = true;
260         } else if (!Arrays.equals(mSelectedPages, selectedPages)) {
261             mSelectedPages = selectedPages;
262             mSelectedPageCount = PageRangeUtils.getNormalizedPageCount(
263                     mSelectedPages, documentPageCount);
264             setConfirmedPages(mSelectedPages, documentPageCount);
265             updatePreviewAreaAndPageSize = true;
266             documentChanged = true;
267         }
268 
269         // If *all pages* is selected we need to convert that to absolute
270         // range as we will be checking if some pages are written or not.
271         if (writtenPages != null) {
272             // If we get all pages, this means all pages that we requested.
273             if (PageRangeUtils.isAllPages(writtenPages)) {
274                 writtenPages = mRequestedPages;
275             }
276             if (!Arrays.equals(mWrittenPages, writtenPages)) {
277                 // TODO: Do a surgical invalidation of only written pages changed.
278                 mWrittenPages = writtenPages;
279                 documentChanged = true;
280             }
281         }
282 
283         if (updatePreviewAreaAndPageSize) {
284             updatePreviewAreaPageSizeAndEmptyState();
285         }
286 
287         if (documentChanged) {
288             notifyDataSetChanged();
289         }
290     }
291 
close(Runnable callback)292     public void close(Runnable callback) {
293         throwIfNotOpened();
294         mState = STATE_CLOSED;
295         if (DEBUG) {
296             Log.i(LOG_TAG, "STATE_CLOSED");
297         }
298         mPageContentRepository.close(callback);
299     }
300 
301     @Override
onCreateViewHolder(ViewGroup parent, int viewType)302     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
303         View page;
304 
305         if (viewType == 0) {
306             page = mLayoutInflater.inflate(R.layout.preview_page_selected, parent, false);
307         } else {
308             page = mLayoutInflater.inflate(R.layout.preview_page, parent, false);
309         }
310 
311         return new MyViewHolder(page);
312     }
313 
314     @Override
onBindViewHolder(ViewHolder holder, int position)315     public void onBindViewHolder(ViewHolder holder, int position) {
316         if (DEBUG) {
317             Log.i(LOG_TAG, "Binding holder: " + holder + " with id: " + getItemId(position)
318                     + " for position: " + position);
319         }
320 
321         MyViewHolder myHolder = (MyViewHolder) holder;
322 
323         PreviewPageFrame page = (PreviewPageFrame) holder.itemView;
324         page.setOnClickListener(mPageClickListener);
325 
326         page.setTag(holder);
327 
328         myHolder.mPageInAdapter = position;
329 
330         final int pageInDocument = computePageIndexInDocument(position);
331         final int pageIndexInFile = computePageIndexInFile(pageInDocument);
332 
333         PageContentView content = (PageContentView) page.findViewById(R.id.page_content);
334 
335         LayoutParams params = content.getLayoutParams();
336         params.width = mPageContentWidth;
337         params.height = mPageContentHeight;
338 
339         PageContentProvider provider = content.getPageContentProvider();
340 
341         if (pageIndexInFile != INVALID_PAGE_INDEX) {
342             if (DEBUG) {
343                 Log.i(LOG_TAG, "Binding provider:"
344                         + " pageIndexInAdapter: " + position
345                         + ", pageInDocument: " + pageInDocument
346                         + ", pageIndexInFile: " + pageIndexInFile);
347             }
348 
349             provider = mPageContentRepository.acquirePageContentProvider(
350                     pageIndexInFile, content);
351             mBoundPagesInAdapter.put(position, null);
352         } else {
353             onSelectedPageNotInFile(pageInDocument);
354         }
355         content.init(provider, mEmptyState, mErrorState, mMediaSize, mMinMargins);
356 
357         if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) >= 0) {
358             page.setSelected(true);
359         } else {
360             page.setSelected(false);
361         }
362 
363         page.setContentDescription(mContext.getString(R.string.page_description_template,
364                 pageInDocument + 1, mDocumentPageCount));
365 
366         TextView pageNumberView = (TextView) page.findViewById(R.id.page_number);
367         String text = mContext.getString(R.string.current_page_template,
368                 pageInDocument + 1, mDocumentPageCount);
369         pageNumberView.setText(text);
370     }
371 
372     @Override
getItemCount()373     public int getItemCount() {
374         return mSelectedPageCount;
375     }
376 
377     @Override
getItemViewType(int position)378     public int getItemViewType(int position) {
379         if (mConfirmedPagesInDocument.indexOfKey(computePageIndexInDocument(position)) >= 0) {
380             return 0;
381         } else {
382             return 1;
383         }
384     }
385 
386     @Override
getItemId(int position)387     public long getItemId(int position) {
388         return computePageIndexInDocument(position);
389     }
390 
391     @Override
onViewRecycled(ViewHolder holder)392     public void onViewRecycled(ViewHolder holder) {
393         MyViewHolder myHolder = (MyViewHolder) holder;
394         PageContentView content = (PageContentView) holder.itemView
395                 .findViewById(R.id.page_content);
396         recyclePageView(content, myHolder.mPageInAdapter);
397         myHolder.mPageInAdapter = INVALID_PAGE_INDEX;
398     }
399 
getRequestedPages()400     public PageRange[] getRequestedPages() {
401         return mRequestedPages;
402     }
403 
getSelectedPages()404     public PageRange[] getSelectedPages() {
405         PageRange[] selectedPages = computeSelectedPages();
406         if (!Arrays.equals(mSelectedPages, selectedPages)) {
407             mSelectedPages = selectedPages;
408             mSelectedPageCount = PageRangeUtils.getNormalizedPageCount(
409                     mSelectedPages, mDocumentPageCount);
410             updatePreviewAreaPageSizeAndEmptyState();
411             notifyDataSetChanged();
412         }
413         return mSelectedPages;
414     }
415 
onPreviewAreaSizeChanged()416     public void onPreviewAreaSizeChanged() {
417         if (mMediaSize != null) {
418             updatePreviewAreaPageSizeAndEmptyState();
419             notifyDataSetChanged();
420         }
421     }
422 
updatePreviewAreaPageSizeAndEmptyState()423     private void updatePreviewAreaPageSizeAndEmptyState() {
424         if (mMediaSize == null) {
425             return;
426         }
427 
428         final int availableWidth = mPreviewArea.getWidth();
429         final int availableHeight = mPreviewArea.getHeight();
430 
431         // Page aspect ratio to keep.
432         final float pageAspectRatio = (float) mMediaSize.getWidthMils()
433                 / mMediaSize.getHeightMils();
434 
435         // Make sure we have no empty columns.
436         final int columnCount = Math.min(mSelectedPageCount, mColumnCount);
437         mPreviewArea.setColumnCount(columnCount);
438 
439         // Compute max page width.
440         final int horizontalMargins = 2 * columnCount * mPreviewPageMargin;
441         final int horizontalPaddingAndMargins = horizontalMargins + 2 * mPreviewListPadding;
442         final int pageContentDesiredWidth = (int) ((((float) availableWidth
443                 - horizontalPaddingAndMargins) / columnCount) + 0.5f);
444 
445         // Compute max page height.
446         final int pageContentDesiredHeight = (int) ((pageContentDesiredWidth
447                 / pageAspectRatio) + 0.5f);
448 
449         // If the page does not fit entirely in a vertical direction,
450         // we shirk it but not less than the minimal page width.
451         final int pageContentMinHeight = (int) (mPreviewPageMinWidth / pageAspectRatio + 0.5f);
452         final int pageContentMaxHeight = Math.max(pageContentMinHeight,
453                 availableHeight - 2 * (mPreviewListPadding + mPreviewPageMargin) - mFooterHeight);
454 
455         mPageContentHeight = Math.min(pageContentDesiredHeight, pageContentMaxHeight);
456         mPageContentWidth = (int) ((mPageContentHeight * pageAspectRatio) + 0.5f);
457 
458         final int totalContentWidth = columnCount * mPageContentWidth + horizontalMargins;
459         final int horizontalPadding = (availableWidth - totalContentWidth) / 2;
460 
461         final int rowCount = mSelectedPageCount / columnCount
462                 + ((mSelectedPageCount % columnCount) > 0 ? 1 : 0);
463         final int totalContentHeight = rowCount * (mPageContentHeight + mFooterHeight + 2
464                 * mPreviewPageMargin);
465 
466         final int verticalPadding;
467         if (mPageContentHeight + mFooterHeight + mPreviewListPadding
468                 + 2 * mPreviewPageMargin > availableHeight) {
469             verticalPadding = Math.max(0,
470                     (availableHeight - mPageContentHeight - mFooterHeight) / 2
471                             - mPreviewPageMargin);
472         } else {
473             verticalPadding = Math.max(mPreviewListPadding,
474                     (availableHeight - totalContentHeight) / 2);
475         }
476 
477         mPreviewArea.setPadding(horizontalPadding, verticalPadding,
478                 horizontalPadding, verticalPadding);
479 
480         // Now update the empty state drawable, as it depends on the page
481         // size and is reused for all views for better performance.
482         LayoutInflater inflater = LayoutInflater.from(mContext);
483         View loadingContent = inflater.inflate(R.layout.preview_page_loading, null, false);
484         loadingContent.measure(MeasureSpec.makeMeasureSpec(mPageContentWidth, MeasureSpec.EXACTLY),
485                 MeasureSpec.makeMeasureSpec(mPageContentHeight, MeasureSpec.EXACTLY));
486         loadingContent.layout(0, 0, loadingContent.getMeasuredWidth(),
487                 loadingContent.getMeasuredHeight());
488 
489         Bitmap loadingBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
490                 Bitmap.Config.ARGB_8888);
491         loadingContent.draw(new Canvas(loadingBitmap));
492 
493         // Do not recycle the old bitmap if such as it may be set as an empty
494         // state to any of the page views. Just let the GC take care of it.
495         mEmptyState = new BitmapDrawable(mContext.getResources(), loadingBitmap);
496 
497         // Now update the empty state drawable, as it depends on the page
498         // size and is reused for all views for better performance.
499         View errorContent = inflater.inflate(R.layout.preview_page_error, null, false);
500         errorContent.measure(MeasureSpec.makeMeasureSpec(mPageContentWidth, MeasureSpec.EXACTLY),
501                 MeasureSpec.makeMeasureSpec(mPageContentHeight, MeasureSpec.EXACTLY));
502         errorContent.layout(0, 0, errorContent.getMeasuredWidth(),
503                 errorContent.getMeasuredHeight());
504 
505         Bitmap errorBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
506                 Bitmap.Config.ARGB_8888);
507         errorContent.draw(new Canvas(errorBitmap));
508 
509         // Do not recycle the old bitmap if such as it may be set as an error
510         // state to any of the page views. Just let the GC take care of it.
511         mErrorState = new BitmapDrawable(mContext.getResources(), errorBitmap);
512     }
513 
computeSelectedPages()514     private PageRange[] computeSelectedPages() {
515         ArrayList<PageRange> selectedPagesList = new ArrayList<>();
516 
517         int startPageIndex = INVALID_PAGE_INDEX;
518         int endPageIndex = INVALID_PAGE_INDEX;
519 
520         final int pageCount = mConfirmedPagesInDocument.size();
521         for (int i = 0; i < pageCount; i++) {
522             final int pageIndex = mConfirmedPagesInDocument.keyAt(i);
523             if (startPageIndex == INVALID_PAGE_INDEX) {
524                 startPageIndex = endPageIndex = pageIndex;
525             }
526             if (endPageIndex + 1 < pageIndex) {
527                 PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
528                 selectedPagesList.add(pageRange);
529                 startPageIndex = pageIndex;
530             }
531             endPageIndex = pageIndex;
532         }
533 
534         if (startPageIndex != INVALID_PAGE_INDEX
535                 && endPageIndex != INVALID_PAGE_INDEX) {
536             PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
537             selectedPagesList.add(pageRange);
538         }
539 
540         PageRange[] selectedPages = new PageRange[selectedPagesList.size()];
541         selectedPagesList.toArray(selectedPages);
542 
543         return selectedPages;
544     }
545 
destroy(Runnable callback)546     public void destroy(Runnable callback) {
547         mCloseGuard.close();
548         mState = STATE_DESTROYED;
549         if (DEBUG) {
550             Log.i(LOG_TAG, "STATE_DESTROYED");
551         }
552         mPageContentRepository.destroy(callback);
553     }
554 
555     @Override
finalize()556     protected void finalize() throws Throwable {
557         try {
558             if (mState != STATE_DESTROYED) {
559                 mCloseGuard.warnIfOpen();
560                 destroy(null);
561             }
562         } finally {
563             super.finalize();
564         }
565     }
566 
computePageIndexInDocument(int indexInAdapter)567     private int computePageIndexInDocument(int indexInAdapter) {
568         int skippedAdapterPages = 0;
569         final int selectedPagesCount = mSelectedPages.length;
570         for (int i = 0; i < selectedPagesCount; i++) {
571             PageRange pageRange = PageRangeUtils.asAbsoluteRange(
572                     mSelectedPages[i], mDocumentPageCount);
573             skippedAdapterPages += pageRange.getSize();
574             if (skippedAdapterPages > indexInAdapter) {
575                 final int overshoot = skippedAdapterPages - indexInAdapter - 1;
576                 return pageRange.getEnd() - overshoot;
577             }
578         }
579         return INVALID_PAGE_INDEX;
580     }
581 
computePageIndexInFile(int pageIndexInDocument)582     private int computePageIndexInFile(int pageIndexInDocument) {
583         if (!PageRangeUtils.contains(mSelectedPages, pageIndexInDocument)) {
584             return INVALID_PAGE_INDEX;
585         }
586         if (mWrittenPages == null) {
587             return INVALID_PAGE_INDEX;
588         }
589 
590         int indexInFile = INVALID_PAGE_INDEX;
591         final int rangeCount = mWrittenPages.length;
592         for (int i = 0; i < rangeCount; i++) {
593             PageRange pageRange = mWrittenPages[i];
594             if (!pageRange.contains(pageIndexInDocument)) {
595                 indexInFile += pageRange.getSize();
596             } else {
597                 indexInFile += pageIndexInDocument - pageRange.getStart() + 1;
598                 return indexInFile;
599             }
600         }
601         return INVALID_PAGE_INDEX;
602     }
603 
setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount)604     private void setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount) {
605         mConfirmedPagesInDocument.clear();
606         final int rangeCount = pagesInDocument.length;
607         for (int i = 0; i < rangeCount; i++) {
608             PageRange pageRange = PageRangeUtils.asAbsoluteRange(pagesInDocument[i],
609                     documentPageCount);
610             for (int j = pageRange.getStart(); j <= pageRange.getEnd(); j++) {
611                 mConfirmedPagesInDocument.put(j, null);
612             }
613         }
614     }
615 
onSelectedPageNotInFile(int pageInDocument)616     private void onSelectedPageNotInFile(int pageInDocument) {
617         PageRange[] requestedPages = computeRequestedPages(pageInDocument);
618         if (!Arrays.equals(mRequestedPages, requestedPages)) {
619             mRequestedPages = requestedPages;
620             if (DEBUG) {
621                 Log.i(LOG_TAG, "Requesting pages: " + Arrays.toString(mRequestedPages));
622             }
623 
624             // This call might come from a recylerview that is currently updating. Hence delay to
625             // after the update
626             (new Handler(Looper.getMainLooper())).post(new Runnable() {
627                 @Override public void run() {
628                     mCallbacks.onRequestContentUpdate();
629                 }
630             });
631         }
632     }
633 
computeRequestedPages(int pageInDocument)634     private PageRange[] computeRequestedPages(int pageInDocument) {
635         if (mRequestedPages != null &&
636                 PageRangeUtils.contains(mRequestedPages, pageInDocument)) {
637             return mRequestedPages;
638         }
639 
640         List<PageRange> pageRangesList = new ArrayList<>();
641 
642         int remainingPagesToRequest = MAX_PREVIEW_PAGES_BATCH;
643         final int selectedPagesCount = mSelectedPages.length;
644 
645         // We always request the pages that are bound, i.e. shown on screen.
646         PageRange[] boundPagesInDocument = computeBoundPagesInDocument();
647 
648         final int boundRangeCount = boundPagesInDocument.length;
649         for (int i = 0; i < boundRangeCount; i++) {
650             PageRange boundRange = boundPagesInDocument[i];
651             pageRangesList.add(boundRange);
652         }
653         remainingPagesToRequest -= PageRangeUtils.getNormalizedPageCount(
654                 boundPagesInDocument, mDocumentPageCount);
655 
656         final boolean requestFromStart = mRequestedPages == null
657                 || pageInDocument > mRequestedPages[mRequestedPages.length - 1].getEnd();
658 
659         if (!requestFromStart) {
660             if (DEBUG) {
661                 Log.i(LOG_TAG, "Requesting from end");
662             }
663 
664             // Reminder that ranges are always normalized.
665             for (int i = selectedPagesCount - 1; i >= 0; i--) {
666                 if (remainingPagesToRequest <= 0) {
667                     break;
668                 }
669 
670                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
671                         mDocumentPageCount);
672                 if (pageInDocument < selectedRange.getStart()) {
673                     continue;
674                 }
675 
676                 PageRange pagesInRange;
677                 int rangeSpan;
678 
679                 if (selectedRange.contains(pageInDocument)) {
680                     rangeSpan = pageInDocument - selectedRange.getStart() + 1;
681                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
682                     final int fromPage = Math.max(pageInDocument - rangeSpan - 1, 0);
683                     rangeSpan = Math.max(rangeSpan, 0);
684                     pagesInRange = new PageRange(fromPage, pageInDocument);
685                 } else {
686                     rangeSpan = selectedRange.getSize();
687                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
688                     rangeSpan = Math.max(rangeSpan, 0);
689                     final int fromPage = Math.max(selectedRange.getEnd() - rangeSpan - 1, 0);
690                     final int toPage = selectedRange.getEnd();
691                     pagesInRange = new PageRange(fromPage, toPage);
692                 }
693 
694                 pageRangesList.add(pagesInRange);
695                 remainingPagesToRequest -= rangeSpan;
696             }
697         } else {
698             if (DEBUG) {
699                 Log.i(LOG_TAG, "Requesting from start");
700             }
701 
702             // Reminder that ranges are always normalized.
703             for (int i = 0; i < selectedPagesCount; i++) {
704                 if (remainingPagesToRequest <= 0) {
705                     break;
706                 }
707 
708                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
709                         mDocumentPageCount);
710                 if (pageInDocument > selectedRange.getEnd()) {
711                     continue;
712                 }
713 
714                 PageRange pagesInRange;
715                 int rangeSpan;
716 
717                 if (selectedRange.contains(pageInDocument)) {
718                     rangeSpan = selectedRange.getEnd() - pageInDocument + 1;
719                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
720                     final int toPage = Math.min(pageInDocument + rangeSpan - 1,
721                             mDocumentPageCount - 1);
722                     pagesInRange = new PageRange(pageInDocument, toPage);
723                 } else {
724                     rangeSpan = selectedRange.getSize();
725                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
726                     final int fromPage = selectedRange.getStart();
727                     final int toPage = Math.min(selectedRange.getStart() + rangeSpan - 1,
728                             mDocumentPageCount - 1);
729                     pagesInRange = new PageRange(fromPage, toPage);
730                 }
731 
732                 if (DEBUG) {
733                     Log.i(LOG_TAG, "computeRequestedPages() Adding range:" + pagesInRange);
734                 }
735                 pageRangesList.add(pagesInRange);
736                 remainingPagesToRequest -= rangeSpan;
737             }
738         }
739 
740         PageRange[] pageRanges = new PageRange[pageRangesList.size()];
741         pageRangesList.toArray(pageRanges);
742 
743         return PageRangeUtils.normalize(pageRanges);
744     }
745 
computeBoundPagesInDocument()746     private PageRange[] computeBoundPagesInDocument() {
747         List<PageRange> pagesInDocumentList = new ArrayList<>();
748 
749         int fromPage = INVALID_PAGE_INDEX;
750         int toPage = INVALID_PAGE_INDEX;
751 
752         final int boundPageCount = mBoundPagesInAdapter.size();
753         for (int i = 0; i < boundPageCount; i++) {
754             // The container is a sparse array, so keys are sorted in ascending order.
755             final int boundPageInAdapter = mBoundPagesInAdapter.keyAt(i);
756             final int boundPageInDocument = computePageIndexInDocument(boundPageInAdapter);
757 
758             if (fromPage == INVALID_PAGE_INDEX) {
759                 fromPage = boundPageInDocument;
760             }
761 
762             if (toPage == INVALID_PAGE_INDEX) {
763                 toPage = boundPageInDocument;
764             }
765 
766             if (boundPageInDocument > toPage + 1) {
767                 PageRange pageRange = new PageRange(fromPage, toPage);
768                 pagesInDocumentList.add(pageRange);
769                 fromPage = toPage = boundPageInDocument;
770             } else {
771                 toPage = boundPageInDocument;
772             }
773         }
774 
775         if (fromPage != INVALID_PAGE_INDEX && toPage != INVALID_PAGE_INDEX) {
776             PageRange pageRange = new PageRange(fromPage, toPage);
777             pagesInDocumentList.add(pageRange);
778         }
779 
780         PageRange[] pageInDocument = new PageRange[pagesInDocumentList.size()];
781         pagesInDocumentList.toArray(pageInDocument);
782 
783         if (DEBUG) {
784             Log.i(LOG_TAG, "Bound pages: " + Arrays.toString(pageInDocument));
785         }
786 
787         return pageInDocument;
788     }
789 
recyclePageView(PageContentView page, int pageIndexInAdapter)790     private void recyclePageView(PageContentView page, int pageIndexInAdapter) {
791         PageContentProvider provider = page.getPageContentProvider();
792         if (provider != null) {
793             page.init(null, mEmptyState, mErrorState, mMediaSize, mMinMargins);
794             mPageContentRepository.releasePageContentProvider(provider);
795         }
796         mBoundPagesInAdapter.remove(pageIndexInAdapter);
797         page.setTag(null);
798     }
799 
startPreloadContent(@onNull PageRange visiblePagesInAdapter)800     void startPreloadContent(@NonNull PageRange visiblePagesInAdapter) {
801         int startVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getStart());
802         int endVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getEnd());
803         if (startVisibleDocument == INVALID_PAGE_INDEX
804                 || endVisibleDocument == INVALID_PAGE_INDEX) {
805             return;
806         }
807 
808         mPageContentRepository.startPreload(new PageRange(startVisibleDocument, endVisibleDocument),
809                 mSelectedPages, mWrittenPages);
810     }
811 
stopPreloadContent()812     public void stopPreloadContent() {
813         mPageContentRepository.stopPreload();
814     }
815 
throwIfNotOpened()816     private void throwIfNotOpened() {
817         if (mState != STATE_OPENED) {
818             throw new IllegalStateException("Not opened");
819         }
820     }
821 
throwIfNotClosed()822     private void throwIfNotClosed() {
823         if (mState != STATE_CLOSED) {
824             throw new IllegalStateException("Not closed");
825         }
826     }
827 
828     private final class MyViewHolder extends ViewHolder {
829         int mPageInAdapter;
830 
MyViewHolder(View itemView)831         private MyViewHolder(View itemView) {
832             super(itemView);
833         }
834     }
835 
836     private final class PageClickListener implements OnClickListener {
837         @Override
onClick(View view)838         public void onClick(View view) {
839             PreviewPageFrame page = (PreviewPageFrame) view;
840             MyViewHolder holder = (MyViewHolder) page.getTag();
841             final int pageInAdapter = holder.mPageInAdapter;
842             final int pageInDocument = computePageIndexInDocument(pageInAdapter);
843             if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) < 0) {
844                 mConfirmedPagesInDocument.put(pageInDocument, null);
845             } else {
846                 if (mConfirmedPagesInDocument.size() <= 1) {
847                     return;
848                 }
849                 mConfirmedPagesInDocument.remove(pageInDocument);
850             }
851 
852             notifyItemChanged(pageInAdapter);
853         }
854     }
855 }
856