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