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         // To create a bitmap, height & width should be larger than 0
490         if (mPageContentHeight <= 0 || mPageContentWidth <= 0) {
491             Log.w(LOG_TAG, "Unable to create bitmap, height or width smaller than 0!");
492             return;
493         }
494 
495         Bitmap loadingBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
496                 Bitmap.Config.ARGB_8888);
497         loadingContent.draw(new Canvas(loadingBitmap));
498 
499         // Do not recycle the old bitmap if such as it may be set as an empty
500         // state to any of the page views. Just let the GC take care of it.
501         mEmptyState = new BitmapDrawable(mContext.getResources(), loadingBitmap);
502 
503         // Now update the empty state drawable, as it depends on the page
504         // size and is reused for all views for better performance.
505         View errorContent = inflater.inflate(R.layout.preview_page_error, null, false);
506         errorContent.measure(MeasureSpec.makeMeasureSpec(mPageContentWidth, MeasureSpec.EXACTLY),
507                 MeasureSpec.makeMeasureSpec(mPageContentHeight, MeasureSpec.EXACTLY));
508         errorContent.layout(0, 0, errorContent.getMeasuredWidth(),
509                 errorContent.getMeasuredHeight());
510 
511         Bitmap errorBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
512                 Bitmap.Config.ARGB_8888);
513         errorContent.draw(new Canvas(errorBitmap));
514 
515         // Do not recycle the old bitmap if such as it may be set as an error
516         // state to any of the page views. Just let the GC take care of it.
517         mErrorState = new BitmapDrawable(mContext.getResources(), errorBitmap);
518     }
519 
computeSelectedPages()520     private PageRange[] computeSelectedPages() {
521         ArrayList<PageRange> selectedPagesList = new ArrayList<>();
522 
523         int startPageIndex = INVALID_PAGE_INDEX;
524         int endPageIndex = INVALID_PAGE_INDEX;
525 
526         final int pageCount = mConfirmedPagesInDocument.size();
527         for (int i = 0; i < pageCount; i++) {
528             final int pageIndex = mConfirmedPagesInDocument.keyAt(i);
529             if (startPageIndex == INVALID_PAGE_INDEX) {
530                 startPageIndex = endPageIndex = pageIndex;
531             }
532             if (endPageIndex + 1 < pageIndex) {
533                 PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
534                 selectedPagesList.add(pageRange);
535                 startPageIndex = pageIndex;
536             }
537             endPageIndex = pageIndex;
538         }
539 
540         if (startPageIndex != INVALID_PAGE_INDEX
541                 && endPageIndex != INVALID_PAGE_INDEX) {
542             PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
543             selectedPagesList.add(pageRange);
544         }
545 
546         PageRange[] selectedPages = new PageRange[selectedPagesList.size()];
547         selectedPagesList.toArray(selectedPages);
548 
549         return selectedPages;
550     }
551 
destroy(Runnable callback)552     public void destroy(Runnable callback) {
553         mCloseGuard.close();
554         mState = STATE_DESTROYED;
555         if (DEBUG) {
556             Log.i(LOG_TAG, "STATE_DESTROYED");
557         }
558         mPageContentRepository.destroy(callback);
559     }
560 
561     @Override
finalize()562     protected void finalize() throws Throwable {
563         try {
564             if (mCloseGuard != null) {
565                 mCloseGuard.warnIfOpen();
566             }
567 
568             if (mState != STATE_DESTROYED) {
569                 destroy(null);
570             }
571         } finally {
572             super.finalize();
573         }
574     }
575 
computePageIndexInDocument(int indexInAdapter)576     private int computePageIndexInDocument(int indexInAdapter) {
577         int skippedAdapterPages = 0;
578         final int selectedPagesCount = mSelectedPages.length;
579         for (int i = 0; i < selectedPagesCount; i++) {
580             PageRange pageRange = PageRangeUtils.asAbsoluteRange(
581                     mSelectedPages[i], mDocumentPageCount);
582             skippedAdapterPages += pageRange.getSize();
583             if (skippedAdapterPages > indexInAdapter) {
584                 final int overshoot = skippedAdapterPages - indexInAdapter - 1;
585                 return pageRange.getEnd() - overshoot;
586             }
587         }
588         return INVALID_PAGE_INDEX;
589     }
590 
computePageIndexInFile(int pageIndexInDocument)591     private int computePageIndexInFile(int pageIndexInDocument) {
592         if (!PageRangeUtils.contains(mSelectedPages, pageIndexInDocument)) {
593             return INVALID_PAGE_INDEX;
594         }
595         if (mWrittenPages == null) {
596             return INVALID_PAGE_INDEX;
597         }
598 
599         int indexInFile = INVALID_PAGE_INDEX;
600         final int rangeCount = mWrittenPages.length;
601         for (int i = 0; i < rangeCount; i++) {
602             PageRange pageRange = mWrittenPages[i];
603             if (!pageRange.contains(pageIndexInDocument)) {
604                 indexInFile += pageRange.getSize();
605             } else {
606                 indexInFile += pageIndexInDocument - pageRange.getStart() + 1;
607                 return indexInFile;
608             }
609         }
610         return INVALID_PAGE_INDEX;
611     }
612 
setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount)613     private void setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount) {
614         mConfirmedPagesInDocument.clear();
615         final int rangeCount = pagesInDocument.length;
616         for (int i = 0; i < rangeCount; i++) {
617             PageRange pageRange = PageRangeUtils.asAbsoluteRange(pagesInDocument[i],
618                     documentPageCount);
619             for (int j = pageRange.getStart(); j <= pageRange.getEnd(); j++) {
620                 mConfirmedPagesInDocument.put(j, null);
621             }
622         }
623     }
624 
onSelectedPageNotInFile(int pageInDocument)625     private void onSelectedPageNotInFile(int pageInDocument) {
626         PageRange[] requestedPages = computeRequestedPages(pageInDocument);
627         if (!Arrays.equals(mRequestedPages, requestedPages)) {
628             mRequestedPages = requestedPages;
629             if (DEBUG) {
630                 Log.i(LOG_TAG, "Requesting pages: " + Arrays.toString(mRequestedPages));
631             }
632 
633             // This call might come from a recylerview that is currently updating. Hence delay to
634             // after the update
635             (new Handler(Looper.getMainLooper())).post(new Runnable() {
636                 @Override public void run() {
637                     mCallbacks.onRequestContentUpdate();
638                 }
639             });
640         }
641     }
642 
computeRequestedPages(int pageInDocument)643     private PageRange[] computeRequestedPages(int pageInDocument) {
644         if (mRequestedPages != null &&
645                 PageRangeUtils.contains(mRequestedPages, pageInDocument)) {
646             return mRequestedPages;
647         }
648 
649         List<PageRange> pageRangesList = new ArrayList<>();
650 
651         int remainingPagesToRequest = MAX_PREVIEW_PAGES_BATCH;
652         final int selectedPagesCount = mSelectedPages.length;
653 
654         // We always request the pages that are bound, i.e. shown on screen.
655         PageRange[] boundPagesInDocument = computeBoundPagesInDocument();
656 
657         final int boundRangeCount = boundPagesInDocument.length;
658         for (int i = 0; i < boundRangeCount; i++) {
659             PageRange boundRange = boundPagesInDocument[i];
660             pageRangesList.add(boundRange);
661         }
662         remainingPagesToRequest -= PageRangeUtils.getNormalizedPageCount(
663                 boundPagesInDocument, mDocumentPageCount);
664 
665         final boolean requestFromStart = mRequestedPages == null
666                 || pageInDocument > mRequestedPages[mRequestedPages.length - 1].getEnd();
667 
668         if (!requestFromStart) {
669             if (DEBUG) {
670                 Log.i(LOG_TAG, "Requesting from end");
671             }
672 
673             // Reminder that ranges are always normalized.
674             for (int i = selectedPagesCount - 1; i >= 0; i--) {
675                 if (remainingPagesToRequest <= 0) {
676                     break;
677                 }
678 
679                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
680                         mDocumentPageCount);
681                 if (pageInDocument < selectedRange.getStart()) {
682                     continue;
683                 }
684 
685                 PageRange pagesInRange;
686                 int rangeSpan;
687 
688                 if (selectedRange.contains(pageInDocument)) {
689                     rangeSpan = pageInDocument - selectedRange.getStart() + 1;
690                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
691                     final int fromPage = Math.max(pageInDocument - rangeSpan - 1, 0);
692                     rangeSpan = Math.max(rangeSpan, 0);
693                     pagesInRange = new PageRange(fromPage, pageInDocument);
694                 } else {
695                     rangeSpan = selectedRange.getSize();
696                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
697                     rangeSpan = Math.max(rangeSpan, 0);
698                     final int fromPage = Math.max(selectedRange.getEnd() - rangeSpan - 1, 0);
699                     final int toPage = selectedRange.getEnd();
700                     pagesInRange = new PageRange(fromPage, toPage);
701                 }
702 
703                 pageRangesList.add(pagesInRange);
704                 remainingPagesToRequest -= rangeSpan;
705             }
706         } else {
707             if (DEBUG) {
708                 Log.i(LOG_TAG, "Requesting from start");
709             }
710 
711             // Reminder that ranges are always normalized.
712             for (int i = 0; i < selectedPagesCount; i++) {
713                 if (remainingPagesToRequest <= 0) {
714                     break;
715                 }
716 
717                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
718                         mDocumentPageCount);
719                 if (pageInDocument > selectedRange.getEnd()) {
720                     continue;
721                 }
722 
723                 PageRange pagesInRange;
724                 int rangeSpan;
725 
726                 if (selectedRange.contains(pageInDocument)) {
727                     rangeSpan = selectedRange.getEnd() - pageInDocument + 1;
728                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
729                     final int toPage = Math.min(pageInDocument + rangeSpan - 1,
730                             mDocumentPageCount - 1);
731                     pagesInRange = new PageRange(pageInDocument, toPage);
732                 } else {
733                     rangeSpan = selectedRange.getSize();
734                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
735                     final int fromPage = selectedRange.getStart();
736                     final int toPage = Math.min(selectedRange.getStart() + rangeSpan - 1,
737                             mDocumentPageCount - 1);
738                     pagesInRange = new PageRange(fromPage, toPage);
739                 }
740 
741                 if (DEBUG) {
742                     Log.i(LOG_TAG, "computeRequestedPages() Adding range:" + pagesInRange);
743                 }
744                 pageRangesList.add(pagesInRange);
745                 remainingPagesToRequest -= rangeSpan;
746             }
747         }
748 
749         PageRange[] pageRanges = new PageRange[pageRangesList.size()];
750         pageRangesList.toArray(pageRanges);
751 
752         return PageRangeUtils.normalize(pageRanges);
753     }
754 
computeBoundPagesInDocument()755     private PageRange[] computeBoundPagesInDocument() {
756         List<PageRange> pagesInDocumentList = new ArrayList<>();
757 
758         int fromPage = INVALID_PAGE_INDEX;
759         int toPage = INVALID_PAGE_INDEX;
760 
761         final int boundPageCount = mBoundPagesInAdapter.size();
762         for (int i = 0; i < boundPageCount; i++) {
763             // The container is a sparse array, so keys are sorted in ascending order.
764             final int boundPageInAdapter = mBoundPagesInAdapter.keyAt(i);
765             final int boundPageInDocument = computePageIndexInDocument(boundPageInAdapter);
766 
767             if (fromPage == INVALID_PAGE_INDEX) {
768                 fromPage = boundPageInDocument;
769             }
770 
771             if (toPage == INVALID_PAGE_INDEX) {
772                 toPage = boundPageInDocument;
773             }
774 
775             if (boundPageInDocument > toPage + 1) {
776                 PageRange pageRange = new PageRange(fromPage, toPage);
777                 pagesInDocumentList.add(pageRange);
778                 fromPage = toPage = boundPageInDocument;
779             } else {
780                 toPage = boundPageInDocument;
781             }
782         }
783 
784         if (fromPage != INVALID_PAGE_INDEX && toPage != INVALID_PAGE_INDEX) {
785             PageRange pageRange = new PageRange(fromPage, toPage);
786             pagesInDocumentList.add(pageRange);
787         }
788 
789         PageRange[] pageInDocument = new PageRange[pagesInDocumentList.size()];
790         pagesInDocumentList.toArray(pageInDocument);
791 
792         if (DEBUG) {
793             Log.i(LOG_TAG, "Bound pages: " + Arrays.toString(pageInDocument));
794         }
795 
796         return pageInDocument;
797     }
798 
recyclePageView(PageContentView page, int pageIndexInAdapter)799     private void recyclePageView(PageContentView page, int pageIndexInAdapter) {
800         PageContentProvider provider = page.getPageContentProvider();
801         if (provider != null) {
802             page.init(null, mEmptyState, mErrorState, mMediaSize, mMinMargins);
803             mPageContentRepository.releasePageContentProvider(provider);
804         }
805         mBoundPagesInAdapter.remove(pageIndexInAdapter);
806         page.setTag(null);
807     }
808 
startPreloadContent(@onNull PageRange visiblePagesInAdapter)809     void startPreloadContent(@NonNull PageRange visiblePagesInAdapter) {
810         int startVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getStart());
811         int endVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getEnd());
812         if (startVisibleDocument == INVALID_PAGE_INDEX
813                 || endVisibleDocument == INVALID_PAGE_INDEX) {
814             return;
815         }
816 
817         mPageContentRepository.startPreload(new PageRange(startVisibleDocument, endVisibleDocument),
818                 mSelectedPages, mWrittenPages);
819     }
820 
stopPreloadContent()821     public void stopPreloadContent() {
822         mPageContentRepository.stopPreload();
823     }
824 
throwIfNotOpened()825     private void throwIfNotOpened() {
826         if (mState != STATE_OPENED) {
827             throw new IllegalStateException("Not opened");
828         }
829     }
830 
throwIfNotClosed()831     private void throwIfNotClosed() {
832         if (mState != STATE_CLOSED) {
833             throw new IllegalStateException("Not closed");
834         }
835     }
836 
837     private final class MyViewHolder extends ViewHolder {
838         int mPageInAdapter;
839 
MyViewHolder(View itemView)840         private MyViewHolder(View itemView) {
841             super(itemView);
842         }
843     }
844 
845     private final class PageClickListener implements OnClickListener {
846         @Override
onClick(View view)847         public void onClick(View view) {
848             PreviewPageFrame page = (PreviewPageFrame) view;
849             MyViewHolder holder = (MyViewHolder) page.getTag();
850             final int pageInAdapter = holder.mPageInAdapter;
851             final int pageInDocument = computePageIndexInDocument(pageInAdapter);
852             if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) < 0) {
853                 mConfirmedPagesInDocument.put(pageInDocument, null);
854             } else {
855                 if (mConfirmedPagesInDocument.size() <= 1) {
856                     return;
857                 }
858                 mConfirmedPagesInDocument.remove(pageInDocument);
859             }
860 
861             notifyItemChanged(pageInAdapter);
862         }
863     }
864 }
865