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.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 {
    120         public void onRequestContentUpdate();
    121         public void onMalformedPdfFile();
    122         public void onSecurePdfFile();
    123     }
    124 
    125     public interface PreviewArea {
    126         public int getWidth();
    127         public int getHeight();
    128         public void setColumnCount(int columnCount);
    129         public void setPadding(int left, int top, int right, int bottom);
    130     }
    131 
    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 
    166     public void onOrientationChanged() {
    167         mColumnCount = mContext.getResources().getInteger(
    168                 R.integer.preview_page_per_row_count);
    169         notifyDataSetChanged();
    170     }
    171 
    172     public boolean isOpened() {
    173         return mState == STATE_OPENED;
    174     }
    175 
    176     public int getFilePageCount() {
    177         return mPageContentRepository.getFilePageCount();
    178     }
    179 
    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 
    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 
    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
    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
    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
    373     public int getItemCount() {
    374         return mSelectedPageCount;
    375     }
    376 
    377     @Override
    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
    387     public long getItemId(int position) {
    388         return computePageIndexInDocument(position);
    389     }
    390 
    391     @Override
    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 
    400     public PageRange[] getRequestedPages() {
    401         return mRequestedPages;
    402     }
    403 
    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 
    416     public void onPreviewAreaSizeChanged() {
    417         if (mMediaSize != null) {
    418             updatePreviewAreaPageSizeAndEmptyState();
    419             notifyDataSetChanged();
    420         }
    421     }
    422 
    423     private void updatePreviewAreaPageSizeAndEmptyState() {
    424         if (mMediaSize == null) {
    425             return;
    426         }
    427 
    428         final int availableWidth = mPreviewArea.getWidth();
    429         final int availableHeight = mPreviewArea.getHeight();
    430 
    431         // Page aspect ratio to keep.
    432         final float pageAspectRatio = (float) mMediaSize.getWidthMils()
    433                 / mMediaSize.getHeightMils();
    434 
    435         // Make sure we have no empty columns.
    436         final int columnCount = Math.min(mSelectedPageCount, mColumnCount);
    437         mPreviewArea.setColumnCount(columnCount);
    438 
    439         // Compute max page width.
    440         final int horizontalMargins = 2 * columnCount * mPreviewPageMargin;
    441         final int horizontalPaddingAndMargins = horizontalMargins + 2 * mPreviewListPadding;
    442         final int pageContentDesiredWidth = (int) ((((float) availableWidth
    443                 - horizontalPaddingAndMargins) / columnCount) + 0.5f);
    444 
    445         // Compute max page height.
    446         final int pageContentDesiredHeight = (int) ((pageContentDesiredWidth
    447                 / pageAspectRatio) + 0.5f);
    448 
    449         // If the page does not fit entirely in a vertical direction,
    450         // we shirk it but not less than the minimal page width.
    451         final int pageContentMinHeight = (int) (mPreviewPageMinWidth / pageAspectRatio + 0.5f);
    452         final int pageContentMaxHeight = Math.max(pageContentMinHeight,
    453                 availableHeight - 2 * (mPreviewListPadding + mPreviewPageMargin) - mFooterHeight);
    454 
    455         mPageContentHeight = Math.min(pageContentDesiredHeight, pageContentMaxHeight);
    456         mPageContentWidth = (int) ((mPageContentHeight * pageAspectRatio) + 0.5f);
    457 
    458         final int totalContentWidth = columnCount * mPageContentWidth + horizontalMargins;
    459         final int horizontalPadding = (availableWidth - totalContentWidth) / 2;
    460 
    461         final int rowCount = mSelectedPageCount / columnCount
    462                 + ((mSelectedPageCount % columnCount) > 0 ? 1 : 0);
    463         final int totalContentHeight = rowCount * (mPageContentHeight + mFooterHeight + 2
    464                 * mPreviewPageMargin);
    465 
    466         final int verticalPadding;
    467         if (mPageContentHeight + mFooterHeight + mPreviewListPadding
    468                 + 2 * mPreviewPageMargin > availableHeight) {
    469             verticalPadding = Math.max(0,
    470                     (availableHeight - mPageContentHeight - mFooterHeight) / 2
    471                             - mPreviewPageMargin);
    472         } else {
    473             verticalPadding = Math.max(mPreviewListPadding,
    474                     (availableHeight - totalContentHeight) / 2);
    475         }
    476 
    477         mPreviewArea.setPadding(horizontalPadding, verticalPadding,
    478                 horizontalPadding, verticalPadding);
    479 
    480         // Now update the empty state drawable, as it depends on the page
    481         // size and is reused for all views for better performance.
    482         LayoutInflater inflater = LayoutInflater.from(mContext);
    483         View loadingContent = inflater.inflate(R.layout.preview_page_loading, null, false);
    484         loadingContent.measure(MeasureSpec.makeMeasureSpec(mPageContentWidth, MeasureSpec.EXACTLY),
    485                 MeasureSpec.makeMeasureSpec(mPageContentHeight, MeasureSpec.EXACTLY));
    486         loadingContent.layout(0, 0, loadingContent.getMeasuredWidth(),
    487                 loadingContent.getMeasuredHeight());
    488 
    489         Bitmap loadingBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
    490                 Bitmap.Config.ARGB_8888);
    491         loadingContent.draw(new Canvas(loadingBitmap));
    492 
    493         // Do not recycle the old bitmap if such as it may be set as an empty
    494         // state to any of the page views. Just let the GC take care of it.
    495         mEmptyState = new BitmapDrawable(mContext.getResources(), loadingBitmap);
    496 
    497         // Now update the empty state drawable, as it depends on the page
    498         // size and is reused for all views for better performance.
    499         View errorContent = inflater.inflate(R.layout.preview_page_error, null, false);
    500         errorContent.measure(MeasureSpec.makeMeasureSpec(mPageContentWidth, MeasureSpec.EXACTLY),
    501                 MeasureSpec.makeMeasureSpec(mPageContentHeight, MeasureSpec.EXACTLY));
    502         errorContent.layout(0, 0, errorContent.getMeasuredWidth(),
    503                 errorContent.getMeasuredHeight());
    504 
    505         Bitmap errorBitmap = Bitmap.createBitmap(mPageContentWidth, mPageContentHeight,
    506                 Bitmap.Config.ARGB_8888);
    507         errorContent.draw(new Canvas(errorBitmap));
    508 
    509         // Do not recycle the old bitmap if such as it may be set as an error
    510         // state to any of the page views. Just let the GC take care of it.
    511         mErrorState = new BitmapDrawable(mContext.getResources(), errorBitmap);
    512     }
    513 
    514     private PageRange[] computeSelectedPages() {
    515         ArrayList<PageRange> selectedPagesList = new ArrayList<>();
    516 
    517         int startPageIndex = INVALID_PAGE_INDEX;
    518         int endPageIndex = INVALID_PAGE_INDEX;
    519 
    520         final int pageCount = mConfirmedPagesInDocument.size();
    521         for (int i = 0; i < pageCount; i++) {
    522             final int pageIndex = mConfirmedPagesInDocument.keyAt(i);
    523             if (startPageIndex == INVALID_PAGE_INDEX) {
    524                 startPageIndex = endPageIndex = pageIndex;
    525             }
    526             if (endPageIndex + 1 < pageIndex) {
    527                 PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
    528                 selectedPagesList.add(pageRange);
    529                 startPageIndex = pageIndex;
    530             }
    531             endPageIndex = pageIndex;
    532         }
    533 
    534         if (startPageIndex != INVALID_PAGE_INDEX
    535                 && endPageIndex != INVALID_PAGE_INDEX) {
    536             PageRange pageRange = new PageRange(startPageIndex, endPageIndex);
    537             selectedPagesList.add(pageRange);
    538         }
    539 
    540         PageRange[] selectedPages = new PageRange[selectedPagesList.size()];
    541         selectedPagesList.toArray(selectedPages);
    542 
    543         return selectedPages;
    544     }
    545 
    546     public void destroy(Runnable callback) {
    547         mCloseGuard.close();
    548         mState = STATE_DESTROYED;
    549         if (DEBUG) {
    550             Log.i(LOG_TAG, "STATE_DESTROYED");
    551         }
    552         mPageContentRepository.destroy(callback);
    553     }
    554 
    555     @Override
    556     protected void finalize() throws Throwable {
    557         try {
    558             if (mState != STATE_DESTROYED) {
    559                 mCloseGuard.warnIfOpen();
    560                 destroy(null);
    561             }
    562         } finally {
    563             super.finalize();
    564         }
    565     }
    566 
    567     private int computePageIndexInDocument(int indexInAdapter) {
    568         int skippedAdapterPages = 0;
    569         final int selectedPagesCount = mSelectedPages.length;
    570         for (int i = 0; i < selectedPagesCount; i++) {
    571             PageRange pageRange = PageRangeUtils.asAbsoluteRange(
    572                     mSelectedPages[i], mDocumentPageCount);
    573             skippedAdapterPages += pageRange.getSize();
    574             if (skippedAdapterPages > indexInAdapter) {
    575                 final int overshoot = skippedAdapterPages - indexInAdapter - 1;
    576                 return pageRange.getEnd() - overshoot;
    577             }
    578         }
    579         return INVALID_PAGE_INDEX;
    580     }
    581 
    582     private int computePageIndexInFile(int pageIndexInDocument) {
    583         if (!PageRangeUtils.contains(mSelectedPages, pageIndexInDocument)) {
    584             return INVALID_PAGE_INDEX;
    585         }
    586         if (mWrittenPages == null) {
    587             return INVALID_PAGE_INDEX;
    588         }
    589 
    590         int indexInFile = INVALID_PAGE_INDEX;
    591         final int rangeCount = mWrittenPages.length;
    592         for (int i = 0; i < rangeCount; i++) {
    593             PageRange pageRange = mWrittenPages[i];
    594             if (!pageRange.contains(pageIndexInDocument)) {
    595                 indexInFile += pageRange.getSize();
    596             } else {
    597                 indexInFile += pageIndexInDocument - pageRange.getStart() + 1;
    598                 return indexInFile;
    599             }
    600         }
    601         return INVALID_PAGE_INDEX;
    602     }
    603 
    604     private void setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount) {
    605         mConfirmedPagesInDocument.clear();
    606         final int rangeCount = pagesInDocument.length;
    607         for (int i = 0; i < rangeCount; i++) {
    608             PageRange pageRange = PageRangeUtils.asAbsoluteRange(pagesInDocument[i],
    609                     documentPageCount);
    610             for (int j = pageRange.getStart(); j <= pageRange.getEnd(); j++) {
    611                 mConfirmedPagesInDocument.put(j, null);
    612             }
    613         }
    614     }
    615 
    616     private void onSelectedPageNotInFile(int pageInDocument) {
    617         PageRange[] requestedPages = computeRequestedPages(pageInDocument);
    618         if (!Arrays.equals(mRequestedPages, requestedPages)) {
    619             mRequestedPages = requestedPages;
    620             if (DEBUG) {
    621                 Log.i(LOG_TAG, "Requesting pages: " + Arrays.toString(mRequestedPages));
    622             }
    623 
    624             // This call might come from a recylerview that is currently updating. Hence delay to
    625             // after the update
    626             (new Handler(Looper.getMainLooper())).post(new Runnable() {
    627                 @Override public void run() {
    628                     mCallbacks.onRequestContentUpdate();
    629                 }
    630             });
    631         }
    632     }
    633 
    634     private PageRange[] computeRequestedPages(int pageInDocument) {
    635         if (mRequestedPages != null &&
    636                 PageRangeUtils.contains(mRequestedPages, pageInDocument)) {
    637             return mRequestedPages;
    638         }
    639 
    640         List<PageRange> pageRangesList = new ArrayList<>();
    641 
    642         int remainingPagesToRequest = MAX_PREVIEW_PAGES_BATCH;
    643         final int selectedPagesCount = mSelectedPages.length;
    644 
    645         // We always request the pages that are bound, i.e. shown on screen.
    646         PageRange[] boundPagesInDocument = computeBoundPagesInDocument();
    647 
    648         final int boundRangeCount = boundPagesInDocument.length;
    649         for (int i = 0; i < boundRangeCount; i++) {
    650             PageRange boundRange = boundPagesInDocument[i];
    651             pageRangesList.add(boundRange);
    652         }
    653         remainingPagesToRequest -= PageRangeUtils.getNormalizedPageCount(
    654                 boundPagesInDocument, mDocumentPageCount);
    655 
    656         final boolean requestFromStart = mRequestedPages == null
    657                 || pageInDocument > mRequestedPages[mRequestedPages.length - 1].getEnd();
    658 
    659         if (!requestFromStart) {
    660             if (DEBUG) {
    661                 Log.i(LOG_TAG, "Requesting from end");
    662             }
    663 
    664             // Reminder that ranges are always normalized.
    665             for (int i = selectedPagesCount - 1; i >= 0; i--) {
    666                 if (remainingPagesToRequest <= 0) {
    667                     break;
    668                 }
    669 
    670                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
    671                         mDocumentPageCount);
    672                 if (pageInDocument < selectedRange.getStart()) {
    673                     continue;
    674                 }
    675 
    676                 PageRange pagesInRange;
    677                 int rangeSpan;
    678 
    679                 if (selectedRange.contains(pageInDocument)) {
    680                     rangeSpan = pageInDocument - selectedRange.getStart() + 1;
    681                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
    682                     final int fromPage = Math.max(pageInDocument - rangeSpan - 1, 0);
    683                     rangeSpan = Math.max(rangeSpan, 0);
    684                     pagesInRange = new PageRange(fromPage, pageInDocument);
    685                 } else {
    686                     rangeSpan = selectedRange.getSize();
    687                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
    688                     rangeSpan = Math.max(rangeSpan, 0);
    689                     final int fromPage = Math.max(selectedRange.getEnd() - rangeSpan - 1, 0);
    690                     final int toPage = selectedRange.getEnd();
    691                     pagesInRange = new PageRange(fromPage, toPage);
    692                 }
    693 
    694                 pageRangesList.add(pagesInRange);
    695                 remainingPagesToRequest -= rangeSpan;
    696             }
    697         } else {
    698             if (DEBUG) {
    699                 Log.i(LOG_TAG, "Requesting from start");
    700             }
    701 
    702             // Reminder that ranges are always normalized.
    703             for (int i = 0; i < selectedPagesCount; i++) {
    704                 if (remainingPagesToRequest <= 0) {
    705                     break;
    706                 }
    707 
    708                 PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i],
    709                         mDocumentPageCount);
    710                 if (pageInDocument > selectedRange.getEnd()) {
    711                     continue;
    712                 }
    713 
    714                 PageRange pagesInRange;
    715                 int rangeSpan;
    716 
    717                 if (selectedRange.contains(pageInDocument)) {
    718                     rangeSpan = selectedRange.getEnd() - pageInDocument + 1;
    719                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
    720                     final int toPage = Math.min(pageInDocument + rangeSpan - 1,
    721                             mDocumentPageCount - 1);
    722                     pagesInRange = new PageRange(pageInDocument, toPage);
    723                 } else {
    724                     rangeSpan = selectedRange.getSize();
    725                     rangeSpan = Math.min(rangeSpan, remainingPagesToRequest);
    726                     final int fromPage = selectedRange.getStart();
    727                     final int toPage = Math.min(selectedRange.getStart() + rangeSpan - 1,
    728                             mDocumentPageCount - 1);
    729                     pagesInRange = new PageRange(fromPage, toPage);
    730                 }
    731 
    732                 if (DEBUG) {
    733                     Log.i(LOG_TAG, "computeRequestedPages() Adding range:" + pagesInRange);
    734                 }
    735                 pageRangesList.add(pagesInRange);
    736                 remainingPagesToRequest -= rangeSpan;
    737             }
    738         }
    739 
    740         PageRange[] pageRanges = new PageRange[pageRangesList.size()];
    741         pageRangesList.toArray(pageRanges);
    742 
    743         return PageRangeUtils.normalize(pageRanges);
    744     }
    745 
    746     private PageRange[] computeBoundPagesInDocument() {
    747         List<PageRange> pagesInDocumentList = new ArrayList<>();
    748 
    749         int fromPage = INVALID_PAGE_INDEX;
    750         int toPage = INVALID_PAGE_INDEX;
    751 
    752         final int boundPageCount = mBoundPagesInAdapter.size();
    753         for (int i = 0; i < boundPageCount; i++) {
    754             // The container is a sparse array, so keys are sorted in ascending order.
    755             final int boundPageInAdapter = mBoundPagesInAdapter.keyAt(i);
    756             final int boundPageInDocument = computePageIndexInDocument(boundPageInAdapter);
    757 
    758             if (fromPage == INVALID_PAGE_INDEX) {
    759                 fromPage = boundPageInDocument;
    760             }
    761 
    762             if (toPage == INVALID_PAGE_INDEX) {
    763                 toPage = boundPageInDocument;
    764             }
    765 
    766             if (boundPageInDocument > toPage + 1) {
    767                 PageRange pageRange = new PageRange(fromPage, toPage);
    768                 pagesInDocumentList.add(pageRange);
    769                 fromPage = toPage = boundPageInDocument;
    770             } else {
    771                 toPage = boundPageInDocument;
    772             }
    773         }
    774 
    775         if (fromPage != INVALID_PAGE_INDEX && toPage != INVALID_PAGE_INDEX) {
    776             PageRange pageRange = new PageRange(fromPage, toPage);
    777             pagesInDocumentList.add(pageRange);
    778         }
    779 
    780         PageRange[] pageInDocument = new PageRange[pagesInDocumentList.size()];
    781         pagesInDocumentList.toArray(pageInDocument);
    782 
    783         if (DEBUG) {
    784             Log.i(LOG_TAG, "Bound pages: " + Arrays.toString(pageInDocument));
    785         }
    786 
    787         return pageInDocument;
    788     }
    789 
    790     private void recyclePageView(PageContentView page, int pageIndexInAdapter) {
    791         PageContentProvider provider = page.getPageContentProvider();
    792         if (provider != null) {
    793             page.init(null, mEmptyState, mErrorState, mMediaSize, mMinMargins);
    794             mPageContentRepository.releasePageContentProvider(provider);
    795         }
    796         mBoundPagesInAdapter.remove(pageIndexInAdapter);
    797         page.setTag(null);
    798     }
    799 
    800     void startPreloadContent(@NonNull PageRange visiblePagesInAdapter) {
    801         int startVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getStart());
    802         int endVisibleDocument = computePageIndexInDocument(visiblePagesInAdapter.getEnd());
    803         if (startVisibleDocument == INVALID_PAGE_INDEX
    804                 || endVisibleDocument == INVALID_PAGE_INDEX) {
    805             return;
    806         }
    807 
    808         mPageContentRepository.startPreload(new PageRange(startVisibleDocument, endVisibleDocument),
    809                 mSelectedPages, mWrittenPages);
    810     }
    811 
    812     public void stopPreloadContent() {
    813         mPageContentRepository.stopPreload();
    814     }
    815 
    816     private void throwIfNotOpened() {
    817         if (mState != STATE_OPENED) {
    818             throw new IllegalStateException("Not opened");
    819         }
    820     }
    821 
    822     private void throwIfNotClosed() {
    823         if (mState != STATE_CLOSED) {
    824             throw new IllegalStateException("Not closed");
    825         }
    826     }
    827 
    828     private final class MyViewHolder extends ViewHolder {
    829         int mPageInAdapter;
    830 
    831         private MyViewHolder(View itemView) {
    832             super(itemView);
    833         }
    834     }
    835 
    836     private final class PageClickListener implements OnClickListener {
    837         @Override
    838         public void onClick(View view) {
    839             PreviewPageFrame page = (PreviewPageFrame) view;
    840             MyViewHolder holder = (MyViewHolder) page.getTag();
    841             final int pageInAdapter = holder.mPageInAdapter;
    842             final int pageInDocument = computePageIndexInDocument(pageInAdapter);
    843             if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) < 0) {
    844                 mConfirmedPagesInDocument.put(pageInDocument, null);
    845             } else {
    846                 if (mConfirmedPagesInDocument.size() <= 1) {
    847                     return;
    848                 }
    849                 mConfirmedPagesInDocument.remove(pageInDocument);
    850             }
    851 
    852             notifyItemChanged(pageInAdapter);
    853         }
    854     }
    855 }
    856