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