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