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