Home | History | Annotate | Download | only in model
      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 {
     87         void onPageContentAvailable(BitmapDrawable content);
     88     }
     89 
     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 
     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 
    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 
    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      */
    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 
    162     public void stopPreload() {
    163         mRenderer.stopPreload();
    164     }
    165 
    166     public int getFilePageCount() {
    167         return mRenderer.getPageCount();
    168     }
    169 
    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 
    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
    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 
    205     private void throwIfNotOpened() {
    206         if (mState != STATE_OPENED) {
    207             throw new IllegalStateException("Not opened");
    208         }
    209     }
    210 
    211     private void throwIfNotClosed() {
    212         if (mState != STATE_CLOSED) {
    213             throw new IllegalStateException("Not closed");
    214         }
    215     }
    216 
    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 
    227         public PageContentProvider(int pageIndex, View owner) {
    228             mPageIndex = pageIndex;
    229             mOwner = owner;
    230         }
    231 
    232         public View getOwner() {
    233             return mOwner;
    234         }
    235 
    236         public int getPageIndex() {
    237             return mPageIndex;
    238         }
    239 
    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 
    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 
    279         public PageContentLruCache(int maxSizeInBytes) {
    280             mMaxSizeInBytes = maxSizeInBytes;
    281         }
    282 
    283         public RenderedPage getRenderedPage(int pageIndex) {
    284             return mRenderedPages.get(pageIndex);
    285         }
    286 
    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 
    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 
    312         public void invalidate() {
    313             for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) {
    314                 entry.getValue().state = RenderedPage.STATE_SCRAP;
    315             }
    316         }
    317 
    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 
    342         public int getSizeInBytes() {
    343             return mSizeInBytes;
    344         }
    345 
    346         public int getMaxSizeInBytes() {
    347             return mMaxSizeInBytes;
    348         }
    349 
    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 
    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
    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 
    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
    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 
    426         RenderedPage(BitmapDrawable content) {
    427             this.content = content;
    428         }
    429 
    430         public int getSizeInBytes() {
    431             return content.getBitmap().getByteCount();
    432         }
    433 
    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 
    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
    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
    476         public void onServiceDisconnected(ComponentName name) {
    477             synchronized (mLock) {
    478                 mRenderer = null;
    479             }
    480         }
    481 
    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 
    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 
    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          */
    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 
    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 
    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 
    635         public int getPageCount() {
    636             return mPageCount;
    637         }
    638 
    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 
    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 
    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 
    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 
    726             public OpenTask(ParcelFileDescriptor source, OpenDocumentCallback callback) {
    727                 mSource = source;
    728                 mCallback = callback;
    729             }
    730 
    731             @Override
    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
    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
    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
    795             protected void onCancelled(Integer integer) {
    796                 mOpenTask = null;
    797             }
    798 
    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 
    814             public RenderPageTask(int pageIndex, RenderSpec renderSpec,
    815                     OnPageContentAvailableCallback callback) {
    816                 mPageIndex = pageIndex;
    817                 mRenderSpec = renderSpec;
    818                 mCallback = callback;
    819             }
    820 
    821             @Override
    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
    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
    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
    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 
    973             public boolean isPreload() {
    974                 return mCallback == null;
    975             }
    976         }
    977     }
    978 }
    979