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