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