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