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 {
onPageContentAvailable(BitmapDrawable content)78         void onPageContentAvailable(BitmapDrawable content);
79     }
80 
PageContentRepository(Context context)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 
open(ParcelFileDescriptor source, final OpenDocumentCallback callback)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 
close(Runnable callback)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 
destroy(final Runnable callback)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 
startPreload(int firstShownPage, int lastShownPage)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 
stopPreload()143     public void stopPreload() {
144         mRenderer.stopPreload();
145     }
146 
getFilePageCount()147     public int getFilePageCount() {
148         return mRenderer.getPageCount();
149     }
150 
acquirePageContentProvider(int pageIndex, View owner)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 
releasePageContentProvider(PageContentProvider provider)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
finalize()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 
throwIfNotOpened()183     private void throwIfNotOpened() {
184         if (mState != STATE_OPENED) {
185             throw new IllegalStateException("Not opened");
186         }
187     }
188 
throwIfNotClosed()189     private void throwIfNotClosed() {
190         if (mState != STATE_CLOSED) {
191             throw new IllegalStateException("Not closed");
192         }
193     }
194 
throwIfDestroyed()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 
PageContentProvider(int pageIndex, View owner)205         public PageContentProvider(int pageIndex, View owner) {
206             mPageIndex = pageIndex;
207             mOwner = owner;
208         }
209 
getOwner()210         public View getOwner() {
211             return mOwner;
212         }
213 
getPageIndex()214         public int getPageIndex() {
215             return mPageIndex;
216         }
217 
getPageContent(RenderSpec renderSpec, OnPageContentAvailableCallback callback)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 
cancelLoad()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 
PageContentLruCache(int maxSizeInBytes)256         public PageContentLruCache(int maxSizeInBytes) {
257             mMaxSizeInBytes = maxSizeInBytes;
258         }
259 
getRenderedPage(int pageIndex)260         public RenderedPage getRenderedPage(int pageIndex) {
261             return mRenderedPages.get(pageIndex);
262         }
263 
removeRenderedPage(int pageIndex)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 
putRenderedPage(int pageIndex, RenderedPage renderedPage)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 
invalidate()289         public void invalidate() {
290             for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) {
291                 entry.getValue().state = RenderedPage.STATE_SCRAP;
292             }
293         }
294 
removeLeastNeeded()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 
getSizeInBytes()319         public int getSizeInBytes() {
320             return mSizeInBytes;
321         }
322 
getMaxSizeInBytes()323         public int getMaxSizeInBytes() {
324             return mMaxSizeInBytes;
325         }
326 
clear()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 
RenderSpec(int bitmapWidth, int bitmapHeight, MediaSize mediaSize, Margins minMargins)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
equals(Object obj)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 
hasSameSize(RenderedPage page)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
hashCode()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 
RenderedPage(BitmapDrawable content)403         RenderedPage(BitmapDrawable content) {
404             this.content = content;
405         }
406 
getSizeInBytes()407         public int getSizeInBytes() {
408             return content.getBitmap().getByteCount();
409         }
410 
erase()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 
AsyncRenderer(Context context)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
onServiceConnected(ComponentName name, IBinder service)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
onServiceDisconnected(ComponentName name)453         public void onServiceDisconnected(ComponentName name) {
454             synchronized (mLock) {
455                 mRenderer = null;
456             }
457         }
458 
open(ParcelFileDescriptor source, OpenDocumentCallback callback)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 
close(final Runnable callback)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 
destroy()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 
startPreload(int firstShownPage, int lastShownPage, RenderSpec renderSpec)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 
stopPreload()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 
getPageCount()572         public int getPageCount() {
573             return mPageCount;
574         }
575 
getCachedPage(int pageIndex, RenderSpec renderSpec, OnPageContentAvailableCallback callback)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 
renderPage(int pageIndex, RenderSpec renderSpec, OnPageContentAvailableCallback callback)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 
cancelRendering(int pageIndex)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 
cancelAllRendering()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 
OpenTask(ParcelFileDescriptor source, OpenDocumentCallback callback)663             public OpenTask(ParcelFileDescriptor source, OpenDocumentCallback callback) {
664                 mSource = source;
665                 mCallback = callback;
666             }
667 
668             @Override
onPreExecute()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
doInBackground(Void... params)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
onPostExecute(Integer pageCount)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
onCancelled(Integer integer)732             protected void onCancelled(Integer integer) {
733                 mOpenTask = null;
734             }
735 
cancel()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 
RenderPageTask(int pageIndex, RenderSpec renderSpec, OnPageContentAvailableCallback callback)751             public RenderPageTask(int pageIndex, RenderSpec renderSpec,
752                     OnPageContentAvailableCallback callback) {
753                 mPageIndex = pageIndex;
754                 mRenderSpec = renderSpec;
755                 mCallback = callback;
756             }
757 
758             @Override
onPreExecute()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
doInBackground(Void... params)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 
842                             mRenderer.renderPage(mPageIndex, bitmap.getWidth(), bitmap.getHeight(),
843                                     mRenderSpec.printAttributes, destination);
844                         }
845 
846                         BitmapSerializeUtils.readBitmapPixels(bitmap, source);
847                     }
848 
849                     mIsFailed = false;
850                 } catch (IOException|RemoteException|IllegalStateException e) {
851                     Log.e(LOG_TAG, "Error rendering page " + mPageIndex, e);
852                     mIsFailed = true;
853                 }
854 
855                 return mRenderedPage;
856             }
857 
858             @Override
onPostExecute(RenderedPage renderedPage)859             public void onPostExecute(RenderedPage renderedPage) {
860                 if (DEBUG) {
861                     Log.i(LOG_TAG, "Completed rendering page: " + mPageIndex);
862                 }
863 
864                 // This task is done.
865                 mPageToRenderTaskMap.remove(mPageIndex);
866 
867                 if (mIsFailed) {
868                     renderedPage.state = RenderedPage.STATE_SCRAP;
869                 } else {
870                     renderedPage.state = RenderedPage.STATE_RENDERED;
871                 }
872 
873                 // Invalidate all caches of the old state of the bitmap
874                 mRenderedPage.content.invalidateSelf();
875 
876                 // Announce success if needed.
877                 if (mCallback != null) {
878                     if (mIsFailed) {
879                         mCallback.onPageContentAvailable(null);
880                     } else {
881                         mCallback.onPageContentAvailable(renderedPage.content);
882                     }
883                 }
884             }
885 
886             @Override
onCancelled(RenderedPage renderedPage)887             protected void onCancelled(RenderedPage renderedPage) {
888                 if (DEBUG) {
889                     Log.i(LOG_TAG, "Cancelled rendering page: " + mPageIndex);
890                 }
891 
892                 // This task is done.
893                 mPageToRenderTaskMap.remove(mPageIndex);
894 
895                 // If canceled before on pre-execute.
896                 if (renderedPage == null) {
897                     return;
898                 }
899 
900                 // Take a note that the content is not rendered.
901                 renderedPage.state = RenderedPage.STATE_SCRAP;
902             }
903 
isPreload()904             public boolean isPreload() {
905                 return mCallback == null;
906             }
907         }
908     }
909 }
910