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