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