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