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.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Canvas; 22 import android.graphics.drawable.BitmapDrawable; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.os.ParcelFileDescriptor; 26 import android.print.PageRange; 27 import android.print.PrintAttributes.MediaSize; 28 import android.print.PrintAttributes.Margins; 29 import android.print.PrintDocumentInfo; 30 import android.support.v7.widget.RecyclerView.Adapter; 31 import android.support.v7.widget.RecyclerView.ViewHolder; 32 import android.util.Log; 33 import android.util.SparseArray; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.View.OnClickListener; 37 import android.view.ViewGroup; 38 import android.view.ViewGroup.LayoutParams; 39 import android.view.View.MeasureSpec; 40 import android.widget.TextView; 41 import com.android.printspooler.R; 42 import com.android.printspooler.model.OpenDocumentCallback; 43 import com.android.printspooler.model.PageContentRepository; 44 import com.android.printspooler.model.PageContentRepository.PageContentProvider; 45 import com.android.printspooler.util.PageRangeUtils; 46 import com.android.printspooler.widget.PageContentView; 47 import com.android.printspooler.widget.PreviewPageFrame; 48 import dalvik.system.CloseGuard; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.List; 53 54 /** 55 * This class represents the adapter for the pages in the print preview list. 56 */ 57 public 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