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