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