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