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 17 package com.android.printspooler.ui; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.drawable.BitmapDrawable; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.ParcelFileDescriptor; 27 import android.print.PageRange; 28 import android.print.PrintAttributes.Margins; 29 import android.print.PrintAttributes.MediaSize; 30 import android.print.PrintDocumentInfo; 31 import android.support.v7.widget.RecyclerView.Adapter; 32 import android.support.v7.widget.RecyclerView.ViewHolder; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.View.MeasureSpec; 38 import android.view.View.OnClickListener; 39 import android.view.ViewGroup; 40 import android.view.ViewGroup.LayoutParams; 41 import android.widget.TextView; 42 43 import com.android.printspooler.R; 44 import com.android.printspooler.model.OpenDocumentCallback; 45 import com.android.printspooler.model.PageContentRepository; 46 import com.android.printspooler.model.PageContentRepository.PageContentProvider; 47 import com.android.printspooler.util.PageRangeUtils; 48 import com.android.printspooler.widget.PageContentView; 49 import com.android.printspooler.widget.PreviewPageFrame; 50 51 import dalvik.system.CloseGuard; 52 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.List; 56 57 /** 58 * This class represents the adapter for the pages in the print preview list. 59 */ 60 public 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 { onRequestContentUpdate()120 public void onRequestContentUpdate(); onMalformedPdfFile()121 public void onMalformedPdfFile(); onSecurePdfFile()122 public void onSecurePdfFile(); 123 } 124 125 public interface PreviewArea { getWidth()126 public int getWidth(); getHeight()127 public int getHeight(); setColumnCount(int columnCount)128 public void setColumnCount(int columnCount); setPadding(int left, int top, int right, int bottom)129 public void setPadding(int left, int top, int right, int bottom); 130 } 131 PageAdapter(Context context, ContentCallbacks callbacks, PreviewArea previewArea)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 onOrientationChanged()166 public void onOrientationChanged() { 167 mColumnCount = mContext.getResources().getInteger( 168 R.integer.preview_page_per_row_count); 169 notifyDataSetChanged(); 170 } 171 isOpened()172 public boolean isOpened() { 173 return mState == STATE_OPENED; 174 } 175 getFilePageCount()176 public int getFilePageCount() { 177 return mPageContentRepository.getFilePageCount(); 178 } 179 open(ParcelFileDescriptor source, final Runnable callback)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 update(PageRange[] writtenPages, PageRange[] selectedPages, int documentPageCount, MediaSize mediaSize, Margins minMargins)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 close(Runnable callback)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 onCreateViewHolder(ViewGroup parent, int viewType)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 onBindViewHolder(ViewHolder holder, int position)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 getItemCount()373 public int getItemCount() { 374 return mSelectedPageCount; 375 } 376 377 @Override getItemViewType(int position)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 getItemId(int position)387 public long getItemId(int position) { 388 return computePageIndexInDocument(position); 389 } 390 391 @Override onViewRecycled(ViewHolder holder)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 getRequestedPages()400 public PageRange[] getRequestedPages() { 401 return mRequestedPages; 402 } 403 getSelectedPages()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 onPreviewAreaSizeChanged()416 public void onPreviewAreaSizeChanged() { 417 if (mMediaSize != null) { 418 updatePreviewAreaPageSizeAndEmptyState(); 419 notifyDataSetChanged(); 420 } 421 } 422 updatePreviewAreaPageSizeAndEmptyState()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 computeSelectedPages()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 destroy(Runnable callback)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 finalize()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 computePageIndexInDocument(int indexInAdapter)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 computePageIndexInFile(int pageIndexInDocument)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 setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount)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 onSelectedPageNotInFile(int pageInDocument)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 computeRequestedPages(int pageInDocument)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 computeBoundPagesInDocument()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 recyclePageView(PageContentView page, int pageIndexInAdapter)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 startPreloadContent(@onNull PageRange visiblePagesInAdapter)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 stopPreloadContent()821 public void stopPreloadContent() { 822 mPageContentRepository.stopPreload(); 823 } 824 throwIfNotOpened()825 private void throwIfNotOpened() { 826 if (mState != STATE_OPENED) { 827 throw new IllegalStateException("Not opened"); 828 } 829 } 830 throwIfNotClosed()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 MyViewHolder(View itemView)840 private MyViewHolder(View itemView) { 841 super(itemView); 842 } 843 } 844 845 private final class PageClickListener implements OnClickListener { 846 @Override onClick(View view)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 } 865