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