PageContentRepository.java revision 13f542cabd635c55ade5442764cc4a3d2f7880ea
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.Rect; 28import android.graphics.drawable.BitmapDrawable; 29import android.os.AsyncTask; 30import android.os.IBinder; 31import android.os.ParcelFileDescriptor; 32import android.os.RemoteException; 33import android.print.PrintAttributes; 34import android.print.PrintDocumentInfo; 35import android.util.ArrayMap; 36import android.util.Log; 37import android.view.View; 38import com.android.internal.annotations.GuardedBy; 39import com.android.printspooler.renderer.IPdfRenderer; 40import com.android.printspooler.renderer.PdfRendererService; 41import dalvik.system.CloseGuard; 42import libcore.io.IoUtils; 43 44import java.io.FileDescriptor; 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; 358 359 public RenderSpec(int bitmapWidth, int bitmapHeight, 360 PrintAttributes printAttributes) { 361 this.bitmapWidth = bitmapWidth; 362 this.bitmapHeight = bitmapHeight; 363 this.printAttributes = printAttributes; 364 } 365 366 @Override 367 public boolean equals(Object obj) { 368 if (this == obj) { 369 return true; 370 } 371 if (obj == null) { 372 return false; 373 } 374 if (getClass() != obj.getClass()) { 375 return false; 376 } 377 RenderSpec other = (RenderSpec) obj; 378 if (bitmapHeight != other.bitmapHeight) { 379 return false; 380 } 381 if (bitmapWidth != other.bitmapWidth) { 382 return false; 383 } 384 if (printAttributes != null) { 385 if (!printAttributes.equals(other.printAttributes)) { 386 return false; 387 } 388 } else if (other.printAttributes != null) { 389 return false; 390 } 391 return true; 392 } 393 394 public boolean hasSameSize(RenderedPage page) { 395 Bitmap bitmap = page.content.getBitmap(); 396 return bitmap.getWidth() == bitmapWidth 397 && bitmap.getHeight() == bitmapHeight; 398 } 399 400 @Override 401 public int hashCode() { 402 int result = bitmapWidth; 403 result = 31 * result + bitmapHeight; 404 result = 31 * result + (printAttributes != null ? printAttributes.hashCode() : 0); 405 return result; 406 } 407 } 408 409 private static final class RenderedPage { 410 public static final int STATE_RENDERED = 0; 411 public static final int STATE_RENDERING = 1; 412 public static final int STATE_SCRAP = 2; 413 414 final BitmapDrawable content; 415 RenderSpec renderSpec; 416 417 int state = STATE_SCRAP; 418 419 RenderedPage(BitmapDrawable content) { 420 this.content = content; 421 } 422 423 public int getSizeInBytes() { 424 return content.getBitmap().getByteCount(); 425 } 426 427 public void recycle() { 428 content.getBitmap().recycle(); 429 } 430 431 public void erase() { 432 content.getBitmap().eraseColor(Color.WHITE); 433 } 434 } 435 436 private static final class AsyncRenderer implements ServiceConnection { 437 private static final int MALFORMED_PDF_FILE_ERROR = -2; 438 439 private final Object mLock = new Object(); 440 441 private final Context mContext; 442 443 private final PageContentLruCache mPageContentCache; 444 445 private final ArrayMap<Integer, RenderPageTask> mPageToRenderTaskMap = new ArrayMap<>(); 446 447 private final OnMalformedPdfFileListener mOnMalformedPdfFileListener; 448 449 private int mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 450 451 @GuardedBy("mLock") 452 private IPdfRenderer mRenderer; 453 454 public AsyncRenderer(Context context, OnMalformedPdfFileListener malformedPdfFileListener) { 455 mContext = context; 456 mOnMalformedPdfFileListener = malformedPdfFileListener; 457 458 ActivityManager activityManager = (ActivityManager) 459 mContext.getSystemService(Context.ACTIVITY_SERVICE); 460 final int cacheSizeInBytes = activityManager.getMemoryClass() * BYTES_PER_MEGABYTE / 4; 461 mPageContentCache = new PageContentLruCache(cacheSizeInBytes); 462 } 463 464 @Override 465 public void onServiceConnected(ComponentName name, IBinder service) { 466 synchronized (mLock) { 467 mRenderer = IPdfRenderer.Stub.asInterface(service); 468 mLock.notifyAll(); 469 } 470 } 471 472 @Override 473 public void onServiceDisconnected(ComponentName name) { 474 synchronized (mLock) { 475 mRenderer = null; 476 } 477 } 478 479 public void open(final ParcelFileDescriptor source, final Runnable callback) { 480 // Opening a new document invalidates the cache as it has pages 481 // from the last document. We keep the cache even when the document 482 // is closed to show pages while the other side is writing the new 483 // document. 484 mPageContentCache.invalidate(); 485 486 new AsyncTask<Void, Void, Integer>() { 487 @Override 488 protected void onPreExecute() { 489 Intent intent = new Intent(mContext, PdfRendererService.class); 490 mContext.bindService(intent, AsyncRenderer.this, Context.BIND_AUTO_CREATE); 491 } 492 493 @Override 494 protected Integer doInBackground(Void... params) { 495 synchronized (mLock) { 496 while (mRenderer == null) { 497 try { 498 mLock.wait(); 499 } catch (InterruptedException ie) { 500 /* ignore */ 501 } 502 } 503 try { 504 return mRenderer.openDocument(source); 505 } catch (RemoteException re) { 506 Log.e(LOG_TAG, "Cannot open PDF document"); 507 return MALFORMED_PDF_FILE_ERROR; 508 } 509 } 510 } 511 512 @Override 513 public void onPostExecute(Integer pageCount) { 514 if (pageCount == MALFORMED_PDF_FILE_ERROR) { 515 mOnMalformedPdfFileListener.onMalformedPdfFile(); 516 mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 517 } else { 518 mPageCount = pageCount; 519 } 520 if (callback != null) { 521 callback.run(); 522 } 523 } 524 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 525 } 526 527 public void close(final Runnable callback) { 528 cancelAllRendering(); 529 530 new AsyncTask<Void, Void, Void>() { 531 @Override 532 protected Void doInBackground(Void... params) { 533 synchronized (mLock) { 534 try { 535 mRenderer.closeDocument(); 536 } catch (RemoteException re) { 537 /* ignore */ 538 } 539 } 540 return null; 541 } 542 543 @Override 544 public void onPostExecute(Void result) { 545 mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; 546 if (callback != null) { 547 callback.run(); 548 } 549 } 550 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 551 } 552 553 public void destroy() { 554 new AsyncTask<Void, Void, Void>() { 555 @Override 556 protected Void doInBackground(Void... params) { 557 return null; 558 } 559 560 @Override 561 public void onPostExecute(Void result) { 562 mContext.unbindService(AsyncRenderer.this); 563 mPageContentCache.invalidate(); 564 mPageContentCache.clear(); 565 } 566 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 567 } 568 569 public void startPreload(int firstShownPage, int lastShownPage, RenderSpec renderSpec) { 570 if (DEBUG) { 571 Log.i(LOG_TAG, "Preloading pages around [" + firstShownPage 572 + "-" + lastShownPage + "]"); 573 } 574 575 final int bitmapSizeInBytes = renderSpec.bitmapWidth * renderSpec.bitmapHeight 576 * BYTES_PER_PIXEL; 577 final int maxCachedPageCount = mPageContentCache.getMaxSizeInBytes() 578 / bitmapSizeInBytes; 579 final int halfPreloadCount = (maxCachedPageCount 580 - (lastShownPage - firstShownPage)) / 2 - 1; 581 582 final int excessFromStart; 583 if (firstShownPage - halfPreloadCount < 0) { 584 excessFromStart = halfPreloadCount - firstShownPage; 585 } else { 586 excessFromStart = 0; 587 } 588 589 final int excessFromEnd; 590 if (lastShownPage + halfPreloadCount >= mPageCount) { 591 excessFromEnd = (lastShownPage + halfPreloadCount) - mPageCount; 592 } else { 593 excessFromEnd = 0; 594 } 595 596 final int fromIndex = Math.max(firstShownPage - halfPreloadCount - excessFromEnd, 0); 597 final int toIndex = Math.min(lastShownPage + halfPreloadCount + excessFromStart, 598 mPageCount - 1); 599 600 for (int i = fromIndex; i <= toIndex; i++) { 601 renderPage(i, renderSpec, null); 602 } 603 } 604 605 public void stopPreload() { 606 final int taskCount = mPageToRenderTaskMap.size(); 607 for (int i = 0; i < taskCount; i++) { 608 RenderPageTask task = mPageToRenderTaskMap.valueAt(i); 609 if (task.isPreload() && !task.isCancelled()) { 610 task.cancel(true); 611 } 612 } 613 } 614 615 public int getPageCount() { 616 return mPageCount; 617 } 618 619 public void getCachedPage(int pageIndex, RenderSpec renderSpec, 620 OnPageContentAvailableCallback callback) { 621 RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); 622 if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED 623 && renderedPage.renderSpec.equals(renderSpec)) { 624 if (DEBUG) { 625 Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); 626 } 627 628 // Announce if needed. 629 if (callback != null) { 630 callback.onPageContentAvailable(renderedPage.content); 631 } 632 } 633 } 634 635 public void renderPage(int pageIndex, RenderSpec renderSpec, 636 OnPageContentAvailableCallback callback) { 637 // First, check if we have a rendered page for this index. 638 RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); 639 if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED) { 640 // If we have rendered page with same constraints - done. 641 if (renderedPage.renderSpec.equals(renderSpec)) { 642 if (DEBUG) { 643 Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); 644 } 645 646 // Announce if needed. 647 if (callback != null) { 648 callback.onPageContentAvailable(renderedPage.content); 649 } 650 return; 651 } else { 652 // If the constraints changed, mark the page obsolete. 653 renderedPage.state = RenderedPage.STATE_SCRAP; 654 } 655 } 656 657 // Next, check if rendering this page is scheduled. 658 RenderPageTask renderTask = mPageToRenderTaskMap.get(pageIndex); 659 if (renderTask != null && !renderTask.isCancelled()) { 660 // If not rendered and constraints same.... 661 if (renderTask.mRenderSpec.equals(renderSpec)) { 662 if (renderTask.mCallback != null) { 663 // If someone else is already waiting for this page - bad state. 664 if (callback != null && renderTask.mCallback != callback) { 665 throw new IllegalStateException("Page rendering not cancelled"); 666 } 667 } else { 668 // No callback means we are preloading so just let the argument 669 // callback be attached to our work in progress. 670 renderTask.mCallback = callback; 671 } 672 return; 673 } else { 674 // If not rendered and constraints changed - cancel rendering. 675 renderTask.cancel(true); 676 } 677 } 678 679 // Oh well, we will have work to do... 680 renderTask = new RenderPageTask(pageIndex, renderSpec, callback); 681 mPageToRenderTaskMap.put(pageIndex, renderTask); 682 renderTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 683 } 684 685 public void cancelRendering(int pageIndex) { 686 RenderPageTask task = mPageToRenderTaskMap.get(pageIndex); 687 if (task != null && !task.isCancelled()) { 688 task.cancel(true); 689 } 690 } 691 692 private void cancelAllRendering() { 693 final int taskCount = mPageToRenderTaskMap.size(); 694 for (int i = 0; i < taskCount; i++) { 695 RenderPageTask task = mPageToRenderTaskMap.valueAt(i); 696 if (!task.isCancelled()) { 697 task.cancel(true); 698 } 699 } 700 } 701 702 private final class RenderPageTask extends AsyncTask<Void, Void, RenderedPage> { 703 final int mPageIndex; 704 final RenderSpec mRenderSpec; 705 OnPageContentAvailableCallback mCallback; 706 RenderedPage mRenderedPage; 707 708 public RenderPageTask(int pageIndex, RenderSpec renderSpec, 709 OnPageContentAvailableCallback callback) { 710 mPageIndex = pageIndex; 711 mRenderSpec = renderSpec; 712 mCallback = callback; 713 } 714 715 @Override 716 protected void onPreExecute() { 717 mRenderedPage = mPageContentCache.getRenderedPage(mPageIndex); 718 if (mRenderedPage != null && mRenderedPage.state == RenderedPage.STATE_RENDERED) { 719 throw new IllegalStateException("Trying to render a rendered page"); 720 } 721 722 // Reuse bitmap for the page only if the right size. 723 if (mRenderedPage != null && !mRenderSpec.hasSameSize(mRenderedPage)) { 724 if (DEBUG) { 725 Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex 726 + " with different size."); 727 } 728 mPageContentCache.removeRenderedPage(mPageIndex); 729 mRenderedPage.recycle(); 730 mRenderedPage = null; 731 } 732 733 final int bitmapSizeInBytes = mRenderSpec.bitmapWidth 734 * mRenderSpec.bitmapHeight * BYTES_PER_PIXEL; 735 736 // Try to find a bitmap to reuse. 737 while (mRenderedPage == null) { 738 739 // Fill the cache greedily. 740 if (mPageContentCache.getSizeInBytes() <= 0 741 || mPageContentCache.getSizeInBytes() + bitmapSizeInBytes 742 <= mPageContentCache.getMaxSizeInBytes()) { 743 break; 744 } 745 746 RenderedPage renderedPage = mPageContentCache.removeLeastNeeded(); 747 748 if (!mRenderSpec.hasSameSize(renderedPage)) { 749 if (DEBUG) { 750 Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex 751 + " with different size."); 752 } 753 renderedPage.recycle(); 754 continue; 755 } 756 757 mRenderedPage = renderedPage; 758 renderedPage.erase(); 759 760 if (DEBUG) { 761 Log.i(LOG_TAG, "Reused bitmap for page: " + mPageIndex + " cache size: " 762 + mPageContentCache.getSizeInBytes() + " bytes"); 763 } 764 765 break; 766 } 767 768 if (mRenderedPage == null) { 769 if (DEBUG) { 770 Log.i(LOG_TAG, "Created bitmap for page: " + mPageIndex + " cache size: " 771 + mPageContentCache.getSizeInBytes() + " bytes"); 772 } 773 Bitmap bitmap = Bitmap.createBitmap(mRenderSpec.bitmapWidth, 774 mRenderSpec.bitmapHeight, Bitmap.Config.ARGB_8888); 775 bitmap.eraseColor(Color.WHITE); 776 BitmapDrawable content = new BitmapDrawable(mContext.getResources(), bitmap); 777 mRenderedPage = new RenderedPage(content); 778 } 779 780 mRenderedPage.renderSpec = mRenderSpec; 781 mRenderedPage.state = RenderedPage.STATE_RENDERING; 782 783 mPageContentCache.putRenderedPage(mPageIndex, mRenderedPage); 784 } 785 786 @Override 787 protected RenderedPage doInBackground(Void... params) { 788 if (isCancelled()) { 789 return mRenderedPage; 790 } 791 792 Bitmap bitmap = mRenderedPage.content.getBitmap(); 793 794 ParcelFileDescriptor[] pipe = null; 795 try { 796 pipe = ParcelFileDescriptor.createPipe(); 797 ParcelFileDescriptor source = pipe[0]; 798 ParcelFileDescriptor destination = pipe[1]; 799 800 mRenderer.renderPage(mPageIndex, bitmap.getWidth(), bitmap.getHeight(), 801 mRenderSpec.printAttributes, destination); 802 803 // We passed the file descriptor to the other side which took 804 // ownership, so close our copy for the write to complete. 805 destination.close(); 806 807 BitmapFactory.Options options = new BitmapFactory.Options(); 808 options.inBitmap = bitmap; 809 BitmapFactory.decodeFileDescriptor(source.getFileDescriptor(), null, options); 810 } catch (IOException|RemoteException e) { 811 Log.e(LOG_TAG, "Error rendering page:" + mPageIndex, e); 812 } finally { 813 IoUtils.closeQuietly(pipe[0]); 814 IoUtils.closeQuietly(pipe[1]); 815 } 816 817 return mRenderedPage; 818 } 819 820 @Override 821 public void onPostExecute(RenderedPage renderedPage) { 822 if (DEBUG) { 823 Log.i(LOG_TAG, "Completed rendering page: " + mPageIndex); 824 } 825 826 // This task is done. 827 mPageToRenderTaskMap.remove(mPageIndex); 828 829 // Take a note that the content is rendered. 830 renderedPage.state = RenderedPage.STATE_RENDERED; 831 832 // Announce success if needed. 833 if (mCallback != null) { 834 mCallback.onPageContentAvailable(renderedPage.content); 835 } 836 } 837 838 @Override 839 protected void onCancelled(RenderedPage renderedPage) { 840 if (DEBUG) { 841 Log.i(LOG_TAG, "Cancelled rendering page: " + mPageIndex); 842 } 843 844 // This task is done. 845 mPageToRenderTaskMap.remove(mPageIndex); 846 847 // If canceled before on pre-execute. 848 if (renderedPage == null) { 849 return; 850 } 851 852 // Take a note that the content is not rendered. 853 renderedPage.state = RenderedPage.STATE_SCRAP; 854 } 855 856 public boolean isPreload() { 857 return mCallback == null; 858 } 859 } 860 } 861} 862