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