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