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