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