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