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