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