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