Home | History | Annotate | Download | only in ui
      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 {
    117         public void onRequestContentUpdate();
    118         public void onMalformedPdfFile();
    119         public void onSecurePdfFile();
    120     }
    121 
    122     public interface PreviewArea {
    123         public int getWidth();
    124         public int getHeight();
    125         public void setColumnCount(int columnCount);
    126         public void setPadding(int left, int top, int right, int bottom);
    127     }
    128 
    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 
    163     public void onOrientationChanged() {
    164         mColumnCount = mContext.getResources().getInteger(
    165                 R.integer.preview_page_per_row_count);
    166         notifyDataSetChanged();
    167     }
    168 
    169     public boolean isOpened() {
    170         return mState == STATE_OPENED;
    171     }
    172 
    173     public int getFilePageCount() {
    174         return mPageContentRepository.getFilePageCount();
    175     }
    176 
    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 
    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 
    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
    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
    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
    363     public int getItemCount() {
    364         return mSelectedPageCount;
    365     }
    366 
    367     @Override
    368     public long getItemId(int position) {
    369         return computePageIndexInDocument(position);
    370     }
    371 
    372     @Override
    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 
    381     public PageRange[] getRequestedPages() {
    382         return mRequestedPages;
    383     }
    384 
    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 
    397     public void onPreviewAreaSizeChanged() {
    398         if (mMediaSize != null) {
    399             updatePreviewAreaPageSizeAndEmptyState();
    400             notifyDataSetChanged();
    401         }
    402     }
    403 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    791     public void stopPreloadContent() {
    792         mPageContentRepository.stopPreload();
    793     }
    794 
    795     private void throwIfNotOpened() {
    796         if (mState != STATE_OPENED) {
    797             throw new IllegalStateException("Not opened");
    798         }
    799     }
    800 
    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 
    810         private MyViewHolder(View itemView) {
    811             super(itemView);
    812         }
    813     }
    814 
    815     private final class PageClickListener implements OnClickListener {
    816         @Override
    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