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.model; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.ServiceConnection; 24 import android.graphics.Bitmap; 25 import android.graphics.Color; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.IBinder; 30 import android.os.ParcelFileDescriptor; 31 import android.os.RemoteException; 32 import android.print.PrintAttributes; 33 import android.print.PrintAttributes.MediaSize; 34 import android.print.PrintAttributes.Margins; 35 import android.print.PrintDocumentInfo; 36 import android.util.ArrayMap; 37 import android.util.Log; 38 import android.view.View; 39 import com.android.internal.annotations.GuardedBy; 40 import com.android.printspooler.renderer.IPdfRenderer; 41 import com.android.printspooler.renderer.PdfManipulationService; 42 import com.android.printspooler.util.BitmapSerializeUtils; 43 import dalvik.system.CloseGuard; 44 import libcore.io.IoUtils; 45 46 import java.io.IOException; 47 import java.util.Iterator; 48 import java.util.LinkedHashMap; 49 import java.util.Map; 50 51 public final class PageContentRepository { 52 private static final String LOG_TAG = "PageContentRepository"; 53 54 private static final boolean DEBUG = false; 55 56 private static final int INVALID_PAGE_INDEX = -1; 57 58 private static final int STATE_CLOSED = 0; 59 private static final int STATE_OPENED = 1; 60 private static final int STATE_DESTROYED = 2; 61 62 private static final int BYTES_PER_PIXEL = 4; 63 64 private static final int BYTES_PER_MEGABYTE = 1048576; 65 66 private final CloseGuard mCloseGuard = CloseGuard.get(); 67 68 private final AsyncRenderer mRenderer; 69 70 private RenderSpec mLastRenderSpec; 71 72 private int mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX; 73 private int mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX; 74 75 private int mState; 76 77 public interface OnPageContentAvailableCallback { 78 void onPageContentAvailable(BitmapDrawable content); 79 } 80 81 public PageContentRepository(Context context) { 82 mRenderer = new AsyncRenderer(context); 83 mState = STATE_CLOSED; 84 if (DEBUG) { 85 Log.i(LOG_TAG, "STATE_CLOSED"); 86 } 87 mCloseGuard.open("destroy"); 88 } 89 90 public void open(ParcelFileDescriptor source, final OpenDocumentCallback callback) { 91 throwIfNotClosed(); 92 mState = STATE_OPENED; 93 if (DEBUG) { 94 Log.i(LOG_TAG, "STATE_OPENED"); 95 } 96 mRenderer.open(source, callback); 97 } 98 99 public void close(Runnable callback) { 100 throwIfNotOpened(); 101 mState = STATE_CLOSED; 102 if (DEBUG) { 103 Log.i(LOG_TAG, "STATE_CLOSED"); 104 } 105 106 mRenderer.close(callback); 107 } 108 109 public void destroy(final Runnable callback) { 110 if (mState == STATE_OPENED) { 111 close(new Runnable() { 112 @Override 113 public void run() { 114 destroy(callback); 115 } 116 }); 117 return; 118 } 119 mCloseGuard.close(); 120 121 mState = STATE_DESTROYED; 122 if (DEBUG) { 123 Log.i(LOG_TAG, "STATE_DESTROYED"); 124 } 125 mRenderer.destroy(); 126 127 if (callback != null) { 128 callback.run(); 129 } 130 } 131 132 public void startPreload(int firstShownPage, int lastShownPage) { 133 // If we do not have a render spec we have no clue what size the 134 // preloaded bitmaps should be, so just take a note for what to do. 135 if (mLastRenderSpec == null) { 136 mScheduledPreloadFirstShownPage = firstShownPage; 137 mScheduledPreloadLastShownPage = lastShownPage; 138 } else if (mState == STATE_OPENED) { 139 mRenderer.startPreload(firstShownPage, lastShownPage, mLastRenderSpec); 140 } 141 } 142 143 public void stopPreload() { 144 mRenderer.stopPreload(); 145 } 146 147 public int getFilePageCount() { 148 return mRenderer.getPageCount(); 149 } 150 151 public PageContentProvider acquirePageContentProvider(int pageIndex, View owner) { 152 throwIfDestroyed(); 153 154 if (DEBUG) { 155 Log.i(LOG_TAG, "Acquiring provider for page: " + pageIndex); 156 } 157 158 return new PageContentProvider(pageIndex, owner); 159 } 160 161 public void releasePageContentProvider(PageContentProvider provider) { 162 throwIfDestroyed(); 163 164 if (DEBUG) { 165 Log.i(LOG_TAG, "Releasing provider for page: " + provider.mPageIndex); 166 } 167 168 provider.cancelLoad(); 169 } 170 171 @Override 172 protected void finalize() throws Throwable { 173 try { 174 if (mState != STATE_DESTROYED) { 175 mCloseGuard.warnIfOpen(); 176 destroy(null); 177 } 178 } finally { 179 super.finalize(); 180 } 181 } 182 183 private void throwIfNotOpened() { 184 if (mState != STATE_OPENED) { 185 throw new IllegalStateException("Not opened"); 186 } 187 } 188 189 private void throwIfNotClosed() { 190 if (mState != STATE_CLOSED) { 191 throw new IllegalStateException("Not closed"); 192 } 193 } 194 195 private void throwIfDestroyed() { 196 if (mState == STATE_DESTROYED) { 197 throw new IllegalStateException("Destroyed"); 198 } 199 } 200 201 public final class PageContentProvider { 202 private final int mPageIndex; 203 private View mOwner; 204 205 public PageContentProvider(int pageIndex, View owner) { 206 mPageIndex = pageIndex; 207 mOwner = owner; 208 } 209 210 public View getOwner() { 211 return mOwner; 212 } 213 214 public int getPageIndex() { 215 return mPageIndex; 216 } 217 218 public void getPageContent(RenderSpec renderSpec, OnPageContentAvailableCallback callback) { 219 throwIfDestroyed(); 220 221 mLastRenderSpec = renderSpec; 222 223 // We tired to preload but didn't know the bitmap size, now 224 // that we know let us do the work. 225 if (mScheduledPreloadFirstShownPage != INVALID_PAGE_INDEX 226 && mScheduledPreloadLastShownPage != INVALID_PAGE_INDEX) { 227 startPreload(mScheduledPreloadFirstShownPage, mScheduledPreloadLastShownPage); 228 mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX; 229 mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX; 230 } 231 232 if (mState == STATE_OPENED) { 233 mRenderer.renderPage(mPageIndex, renderSpec, callback); 234 } else { 235 mRenderer.getCachedPage(mPageIndex, renderSpec, callback); 236 } 237 } 238 239 void cancelLoad() { 240 throwIfDestroyed(); 241 242 if (mState == STATE_OPENED) { 243 mRenderer.cancelRendering(mPageIndex); 244 } 245 } 246 } 247 248 private static final class PageContentLruCache { 249 private final LinkedHashMap<Integer, RenderedPage> mRenderedPages = 250 new LinkedHashMap<>(); 251 252 private final int mMaxSizeInBytes; 253 254 private int mSizeInBytes; 255 256 public PageContentLruCache(int maxSizeInBytes) { 257 mMaxSizeInBytes = maxSizeInBytes; 258 } 259 260 public RenderedPage getRenderedPage(int pageIndex) { 261 return mRenderedPages.get(pageIndex); 262 } 263 264 public RenderedPage removeRenderedPage(int pageIndex) { 265 RenderedPage page = mRenderedPages.remove(pageIndex); 266 if (page != null) { 267 mSizeInBytes -= page.getSizeInBytes(); 268 } 269 return page; 270 } 271 272 public RenderedPage putRenderedPage(int pageIndex, RenderedPage renderedPage) { 273 RenderedPage oldRenderedPage = mRenderedPages.remove(pageIndex); 274 if (oldRenderedPage != null) { 275 if (!oldRenderedPage.renderSpec.equals(renderedPage.renderSpec)) { 276 throw new IllegalStateException("Wrong page size"); 277 } 278 } else { 279 final int contentSizeInBytes = renderedPage.getSizeInBytes(); 280 if (mSizeInBytes + contentSizeInBytes > mMaxSizeInBytes) { 281 throw new IllegalStateException("Client didn't free space"); 282 } 283 284 mSizeInBytes += contentSizeInBytes; 285 } 286 return mRenderedPages.put(pageIndex, renderedPage); 287 } 288 289 public void invalidate() { 290 for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) { 291 entry.getValue().state = RenderedPage.STATE_SCRAP; 292 } 293 } 294 295 public RenderedPage removeLeastNeeded() { 296 if (mRenderedPages.isEmpty()) { 297 return null; 298 } 299 300 // First try to remove a rendered page that holds invalidated 301 // or incomplete content, i.e. its render spec is null. 302 for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) { 303 RenderedPage renderedPage = entry.getValue(); 304 if (renderedPage.state == RenderedPage.STATE_SCRAP) { 305 Integer pageIndex = entry.getKey(); 306 mRenderedPages.remove(pageIndex); 307 mSizeInBytes -= renderedPage.getSizeInBytes(); 308 return renderedPage; 309 } 310 } 311 312 // If all rendered pages contain rendered content, then use the oldest. 313 final int pageIndex = mRenderedPages.eldest().getKey(); 314 RenderedPage renderedPage = mRenderedPages.remove(pageIndex); 315 mSizeInBytes -= renderedPage.getSizeInBytes(); 316 return renderedPage; 317 } 318 319 public int getSizeInBytes() { 320 return mSizeInBytes; 321 } 322 323 public int getMaxSizeInBytes() { 324 return mMaxSizeInBytes; 325 } 326 327 public void clear() { 328 Iterator<Map.Entry<Integer, RenderedPage>> iterator = 329 mRenderedPages.entrySet().iterator(); 330 while (iterator.hasNext()) { 331 iterator.next(); 332 iterator.remove(); 333 } 334 } 335 } 336 337 public static final class RenderSpec { 338 final int bitmapWidth; 339 final int bitmapHeight; 340 final PrintAttributes printAttributes = new PrintAttributes.Builder().build(); 341 342 public RenderSpec(int bitmapWidth, int bitmapHeight, 343 MediaSize mediaSize, Margins minMargins) { 344 this.bitmapWidth = bitmapWidth; 345 this.bitmapHeight = bitmapHeight; 346 printAttributes.setMediaSize(mediaSize); 347 printAttributes.setMinMargins(minMargins); 348 } 349 350 @Override 351 public boolean equals(Object obj) { 352 if (this == obj) { 353 return true; 354 } 355 if (obj == null) { 356 return false; 357 } 358 if (getClass() != obj.getClass()) { 359 return false; 360 } 361 RenderSpec other = (RenderSpec) obj; 362 if (bitmapHeight != other.bitmapHeight) { 363 return false; 364 } 365 if (bitmapWidth != other.bitmapWidth) { 366 return false; 367 } 368 if (printAttributes != null) { 369 if (!printAttributes.equals(other.printAttributes)) { 370 return false; 371 } 372 } else if (other.printAttributes != null) { 373 return false; 374 } 375 return true; 376 } 377 378 public boolean hasSameSize(RenderedPage page) { 379 Bitmap bitmap = page.content.getBitmap(); 380 return bitmap.getWidth() == bitmapWidth 381 && bitmap.getHeight() == bitmapHeight; 382 } 383 384 @Override 385 public int hashCode() { 386 int result = bitmapWidth; 387 result = 31 * result + bitmapHeight; 388 result = 31 * result + (printAttributes != null ? printAttributes.hashCode() : 0); 389 return result; 390 } 391 } 392 393 private static final class RenderedPage { 394 public static final int STATE_RENDERED = 0; 395 public static final int STATE_RENDERING = 1; 396 public static final int STATE_SCRAP = 2; 397 398 final BitmapDrawable content; 399 RenderSpec renderSpec; 400 401 int state = STATE_SCRAP; 402 403 RenderedPage(BitmapDrawable content) { 404 this.content = content; 405 } 406 407 public int getSizeInBytes() { 408 return content.getBitmap().getByteCount(); 409 } 410 411 public void erase() { 412 content.getBitmap().eraseColor(Color.WHITE); 413 } 414 } 415 416 private static final class AsyncRenderer implements ServiceConnection { 417 private final Object mLock = new Object(); 418 419 private final Context mContext; 420 421 private final PageContentLruCache mPageContentCache; 422 423 private final ArrayMap<Integer, RenderPageTask> mPageToRenderTaskMap = new ArrayMap<>(); 424 425 private int mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 426 427 @GuardedBy("mLock") 428 private IPdfRenderer mRenderer; 429 430 private OpenTask mOpenTask; 431 432 private boolean mBoundToService; 433 private boolean mDestroyed; 434 435 public AsyncRenderer(Context context) { 436 mContext = context; 437 438 ActivityManager activityManager = (ActivityManager) 439 mContext.getSystemService(Context.ACTIVITY_SERVICE); 440 final int cacheSizeInBytes = activityManager.getMemoryClass() * BYTES_PER_MEGABYTE / 4; 441 mPageContentCache = new PageContentLruCache(cacheSizeInBytes); 442 } 443 444 @Override 445 public void onServiceConnected(ComponentName name, IBinder service) { 446 synchronized (mLock) { 447 mRenderer = IPdfRenderer.Stub.asInterface(service); 448 mLock.notifyAll(); 449 } 450 } 451 452 @Override 453 public void onServiceDisconnected(ComponentName name) { 454 synchronized (mLock) { 455 mRenderer = null; 456 } 457 } 458 459 public void open(ParcelFileDescriptor source, OpenDocumentCallback callback) { 460 // Opening a new document invalidates the cache as it has pages 461 // from the last document. We keep the cache even when the document 462 // is closed to show pages while the other side is writing the new 463 // document. 464 mPageContentCache.invalidate(); 465 466 mOpenTask = new OpenTask(source, callback); 467 mOpenTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 468 } 469 470 public void close(final Runnable callback) { 471 cancelAllRendering(); 472 473 if (mOpenTask != null) { 474 mOpenTask.cancel(); 475 } 476 477 new AsyncTask<Void, Void, Void>() { 478 @Override 479 protected void onPreExecute() { 480 if (mDestroyed) { 481 cancel(true); 482 return; 483 } 484 } 485 486 @Override 487 protected Void doInBackground(Void... params) { 488 synchronized (mLock) { 489 try { 490 if (mRenderer != null) { 491 mRenderer.closeDocument(); 492 } 493 } catch (RemoteException re) { 494 /* ignore */ 495 } 496 } 497 return null; 498 } 499 500 @Override 501 public void onPostExecute(Void result) { 502 mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 503 if (callback != null) { 504 callback.run(); 505 } 506 } 507 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 508 } 509 510 public void destroy() { 511 if (mBoundToService) { 512 mBoundToService = false; 513 try { 514 mContext.unbindService(AsyncRenderer.this); 515 } catch (IllegalArgumentException e) { 516 // Service might have been forcefully unbound in onDestroy() 517 Log.e(LOG_TAG, "Cannot unbind service", e); 518 } 519 } 520 521 mPageContentCache.invalidate(); 522 mPageContentCache.clear(); 523 mDestroyed = true; 524 } 525 526 public void startPreload(int firstShownPage, int lastShownPage, RenderSpec renderSpec) { 527 if (DEBUG) { 528 Log.i(LOG_TAG, "Preloading pages around [" + firstShownPage 529 + "-" + lastShownPage + "]"); 530 } 531 532 final int bitmapSizeInBytes = renderSpec.bitmapWidth * renderSpec.bitmapHeight 533 * BYTES_PER_PIXEL; 534 final int maxCachedPageCount = mPageContentCache.getMaxSizeInBytes() 535 / bitmapSizeInBytes; 536 final int halfPreloadCount = (maxCachedPageCount 537 - (lastShownPage - firstShownPage)) / 2 - 1; 538 539 final int excessFromStart; 540 if (firstShownPage - halfPreloadCount < 0) { 541 excessFromStart = halfPreloadCount - firstShownPage; 542 } else { 543 excessFromStart = 0; 544 } 545 546 final int excessFromEnd; 547 if (lastShownPage + halfPreloadCount >= mPageCount) { 548 excessFromEnd = (lastShownPage + halfPreloadCount) - mPageCount; 549 } else { 550 excessFromEnd = 0; 551 } 552 553 final int fromIndex = Math.max(firstShownPage - halfPreloadCount - excessFromEnd, 0); 554 final int toIndex = Math.min(lastShownPage + halfPreloadCount + excessFromStart, 555 mPageCount - 1); 556 557 for (int i = fromIndex; i <= toIndex; i++) { 558 renderPage(i, renderSpec, null); 559 } 560 } 561 562 public void stopPreload() { 563 final int taskCount = mPageToRenderTaskMap.size(); 564 for (int i = 0; i < taskCount; i++) { 565 RenderPageTask task = mPageToRenderTaskMap.valueAt(i); 566 if (task.isPreload() && !task.isCancelled()) { 567 task.cancel(true); 568 } 569 } 570 } 571 572 public int getPageCount() { 573 return mPageCount; 574 } 575 576 public void getCachedPage(int pageIndex, RenderSpec renderSpec, 577 OnPageContentAvailableCallback callback) { 578 RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); 579 if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED 580 && renderedPage.renderSpec.equals(renderSpec)) { 581 if (DEBUG) { 582 Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); 583 } 584 585 // Announce if needed. 586 if (callback != null) { 587 callback.onPageContentAvailable(renderedPage.content); 588 } 589 } 590 } 591 592 public void renderPage(int pageIndex, RenderSpec renderSpec, 593 OnPageContentAvailableCallback callback) { 594 // First, check if we have a rendered page for this index. 595 RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); 596 if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED) { 597 // If we have rendered page with same constraints - done. 598 if (renderedPage.renderSpec.equals(renderSpec)) { 599 if (DEBUG) { 600 Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); 601 } 602 603 // Announce if needed. 604 if (callback != null) { 605 callback.onPageContentAvailable(renderedPage.content); 606 } 607 return; 608 } else { 609 // If the constraints changed, mark the page obsolete. 610 renderedPage.state = RenderedPage.STATE_SCRAP; 611 } 612 } 613 614 // Next, check if rendering this page is scheduled. 615 RenderPageTask renderTask = mPageToRenderTaskMap.get(pageIndex); 616 if (renderTask != null && !renderTask.isCancelled()) { 617 // If not rendered and constraints same.... 618 if (renderTask.mRenderSpec.equals(renderSpec)) { 619 if (renderTask.mCallback != null) { 620 // If someone else is already waiting for this page - bad state. 621 if (callback != null && renderTask.mCallback != callback) { 622 throw new IllegalStateException("Page rendering not cancelled"); 623 } 624 } else { 625 // No callback means we are preloading so just let the argument 626 // callback be attached to our work in progress. 627 renderTask.mCallback = callback; 628 } 629 return; 630 } else { 631 // If not rendered and constraints changed - cancel rendering. 632 renderTask.cancel(true); 633 } 634 } 635 636 // Oh well, we will have work to do... 637 renderTask = new RenderPageTask(pageIndex, renderSpec, callback); 638 mPageToRenderTaskMap.put(pageIndex, renderTask); 639 renderTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 640 } 641 642 public void cancelRendering(int pageIndex) { 643 RenderPageTask task = mPageToRenderTaskMap.get(pageIndex); 644 if (task != null && !task.isCancelled()) { 645 task.cancel(true); 646 } 647 } 648 649 private void cancelAllRendering() { 650 final int taskCount = mPageToRenderTaskMap.size(); 651 for (int i = 0; i < taskCount; i++) { 652 RenderPageTask task = mPageToRenderTaskMap.valueAt(i); 653 if (!task.isCancelled()) { 654 task.cancel(true); 655 } 656 } 657 } 658 659 private final class OpenTask extends AsyncTask<Void, Void, Integer> { 660 private final ParcelFileDescriptor mSource; 661 private final OpenDocumentCallback mCallback; 662 663 public OpenTask(ParcelFileDescriptor source, OpenDocumentCallback callback) { 664 mSource = source; 665 mCallback = callback; 666 } 667 668 @Override 669 protected void onPreExecute() { 670 if (mDestroyed) { 671 cancel(true); 672 return; 673 } 674 Intent intent = new Intent(PdfManipulationService.ACTION_GET_RENDERER); 675 intent.setClass(mContext, PdfManipulationService.class); 676 intent.setData(Uri.fromParts("fake-scheme", String.valueOf( 677 AsyncRenderer.this.hashCode()), null)); 678 mContext.bindService(intent, AsyncRenderer.this, Context.BIND_AUTO_CREATE); 679 mBoundToService = true; 680 } 681 682 @Override 683 protected Integer doInBackground(Void... params) { 684 synchronized (mLock) { 685 while (mRenderer == null && !isCancelled()) { 686 try { 687 mLock.wait(); 688 } catch (InterruptedException ie) { 689 /* ignore */ 690 } 691 } 692 try { 693 return mRenderer.openDocument(mSource); 694 } catch (RemoteException re) { 695 Log.e(LOG_TAG, "Cannot open PDF document"); 696 return PdfManipulationService.ERROR_MALFORMED_PDF_FILE; 697 } finally { 698 // Close the fd as we passed it to another process 699 // which took ownership. 700 IoUtils.closeQuietly(mSource); 701 } 702 } 703 } 704 705 @Override 706 public void onPostExecute(Integer pageCount) { 707 switch (pageCount) { 708 case PdfManipulationService.ERROR_MALFORMED_PDF_FILE: { 709 mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 710 if (mCallback != null) { 711 mCallback.onFailure(OpenDocumentCallback.ERROR_MALFORMED_PDF_FILE); 712 } 713 } break; 714 case PdfManipulationService.ERROR_SECURE_PDF_FILE: { 715 mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 716 if (mCallback != null) { 717 mCallback.onFailure(OpenDocumentCallback.ERROR_SECURE_PDF_FILE); 718 } 719 } break; 720 default: { 721 mPageCount = pageCount; 722 if (mCallback != null) { 723 mCallback.onSuccess(); 724 } 725 } break; 726 } 727 728 mOpenTask = null; 729 } 730 731 @Override 732 protected void onCancelled(Integer integer) { 733 mOpenTask = null; 734 } 735 736 public void cancel() { 737 cancel(true); 738 synchronized(mLock) { 739 mLock.notifyAll(); 740 } 741 } 742 } 743 744 private final class RenderPageTask extends AsyncTask<Void, Void, RenderedPage> { 745 final int mPageIndex; 746 final RenderSpec mRenderSpec; 747 OnPageContentAvailableCallback mCallback; 748 RenderedPage mRenderedPage; 749 private boolean mIsFailed; 750 751 public RenderPageTask(int pageIndex, RenderSpec renderSpec, 752 OnPageContentAvailableCallback callback) { 753 mPageIndex = pageIndex; 754 mRenderSpec = renderSpec; 755 mCallback = callback; 756 } 757 758 @Override 759 protected void onPreExecute() { 760 mRenderedPage = mPageContentCache.getRenderedPage(mPageIndex); 761 if (mRenderedPage != null && mRenderedPage.state == RenderedPage.STATE_RENDERED) { 762 throw new IllegalStateException("Trying to render a rendered page"); 763 } 764 765 // Reuse bitmap for the page only if the right size. 766 if (mRenderedPage != null && !mRenderSpec.hasSameSize(mRenderedPage)) { 767 if (DEBUG) { 768 Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex 769 + " with different size."); 770 } 771 mPageContentCache.removeRenderedPage(mPageIndex); 772 mRenderedPage = null; 773 } 774 775 final int bitmapSizeInBytes = mRenderSpec.bitmapWidth 776 * mRenderSpec.bitmapHeight * BYTES_PER_PIXEL; 777 778 // Try to find a bitmap to reuse. 779 while (mRenderedPage == null) { 780 781 // Fill the cache greedily. 782 if (mPageContentCache.getSizeInBytes() <= 0 783 || mPageContentCache.getSizeInBytes() + bitmapSizeInBytes 784 <= mPageContentCache.getMaxSizeInBytes()) { 785 break; 786 } 787 788 RenderedPage renderedPage = mPageContentCache.removeLeastNeeded(); 789 790 if (!mRenderSpec.hasSameSize(renderedPage)) { 791 if (DEBUG) { 792 Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex 793 + " with different size."); 794 } 795 continue; 796 } 797 798 mRenderedPage = renderedPage; 799 renderedPage.erase(); 800 801 if (DEBUG) { 802 Log.i(LOG_TAG, "Reused bitmap for page: " + mPageIndex + " cache size: " 803 + mPageContentCache.getSizeInBytes() + " bytes"); 804 } 805 806 break; 807 } 808 809 if (mRenderedPage == null) { 810 if (DEBUG) { 811 Log.i(LOG_TAG, "Created bitmap for page: " + mPageIndex + " cache size: " 812 + mPageContentCache.getSizeInBytes() + " bytes"); 813 } 814 Bitmap bitmap = Bitmap.createBitmap(mRenderSpec.bitmapWidth, 815 mRenderSpec.bitmapHeight, Bitmap.Config.ARGB_8888); 816 bitmap.eraseColor(Color.WHITE); 817 BitmapDrawable content = new BitmapDrawable(mContext.getResources(), bitmap); 818 mRenderedPage = new RenderedPage(content); 819 } 820 821 mRenderedPage.renderSpec = mRenderSpec; 822 mRenderedPage.state = RenderedPage.STATE_RENDERING; 823 824 mPageContentCache.putRenderedPage(mPageIndex, mRenderedPage); 825 } 826 827 @Override 828 protected RenderedPage doInBackground(Void... params) { 829 if (isCancelled()) { 830 return mRenderedPage; 831 } 832 833 Bitmap bitmap = mRenderedPage.content.getBitmap(); 834 835 ParcelFileDescriptor[] pipe; 836 try { 837 pipe = ParcelFileDescriptor.createPipe(); 838 839 try (ParcelFileDescriptor source = pipe[0]) { 840 try (ParcelFileDescriptor destination = pipe[1]) { 841 synchronized (mLock) { 842 if (mRenderer != null) { 843 mRenderer.renderPage(mPageIndex, bitmap.getWidth(), 844 bitmap.getHeight(), mRenderSpec.printAttributes, 845 destination); 846 } else { 847 throw new IllegalStateException("Renderer is disconnected"); 848 } 849 } 850 } 851 852 BitmapSerializeUtils.readBitmapPixels(bitmap, source); 853 } 854 855 mIsFailed = false; 856 } catch (IOException|RemoteException|IllegalStateException e) { 857 Log.e(LOG_TAG, "Error rendering page " + mPageIndex, e); 858 mIsFailed = true; 859 } 860 861 return mRenderedPage; 862 } 863 864 @Override 865 public void onPostExecute(RenderedPage renderedPage) { 866 if (DEBUG) { 867 Log.i(LOG_TAG, "Completed rendering page: " + mPageIndex); 868 } 869 870 // This task is done. 871 mPageToRenderTaskMap.remove(mPageIndex); 872 873 if (mIsFailed) { 874 renderedPage.state = RenderedPage.STATE_SCRAP; 875 } else { 876 renderedPage.state = RenderedPage.STATE_RENDERED; 877 } 878 879 // Invalidate all caches of the old state of the bitmap 880 mRenderedPage.content.invalidateSelf(); 881 882 // Announce success if needed. 883 if (mCallback != null) { 884 if (mIsFailed) { 885 mCallback.onPageContentAvailable(null); 886 } else { 887 mCallback.onPageContentAvailable(renderedPage.content); 888 } 889 } 890 } 891 892 @Override 893 protected void onCancelled(RenderedPage renderedPage) { 894 if (DEBUG) { 895 Log.i(LOG_TAG, "Cancelled rendering page: " + mPageIndex); 896 } 897 898 // This task is done. 899 mPageToRenderTaskMap.remove(mPageIndex); 900 901 // If canceled before on pre-execute. 902 if (renderedPage == null) { 903 return; 904 } 905 906 // Take a note that the content is not rendered. 907 renderedPage.state = RenderedPage.STATE_SCRAP; 908 } 909 910 public boolean isPreload() { 911 return mCallback == null; 912 } 913 } 914 } 915 } 916