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         // 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 
    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 
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    821     public void stopPreloadContent() {
    822         mPageContentRepository.stopPreload();
    823     }
    824 
    825     private void throwIfNotOpened() {
    826         if (mState != STATE_OPENED) {
    827             throw new IllegalStateException("Not opened");
    828         }
    829     }
    830 
    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 
    840         private MyViewHolder(View itemView) {
    841             super(itemView);
    842         }
    843     }
    844 
    845     private final class PageClickListener implements OnClickListener {
    846         @Override
    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