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