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