OSDN Git Service

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