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