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