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