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