/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.printspooler.model;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.print.PrintAttributes;
import android.print.PrintAttributes.MediaSize;
import android.print.PrintAttributes.Margins;
import android.print.PrintDocumentInfo;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import com.android.internal.annotations.GuardedBy;
import com.android.printspooler.renderer.IPdfRenderer;
import com.android.printspooler.renderer.PdfManipulationService;
import com.android.printspooler.util.BitmapSerializeUtils;
import dalvik.system.CloseGuard;
import libcore.io.IoUtils;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public final class PageContentRepository {
    private static final String LOG_TAG = "PageContentRepository";

    private static final boolean DEBUG = false;

    private static final int INVALID_PAGE_INDEX = -1;

    private static final int STATE_CLOSED = 0;
    private static final int STATE_OPENED = 1;
    private static final int STATE_DESTROYED = 2;

    private static final int BYTES_PER_PIXEL = 4;

    private static final int BYTES_PER_MEGABYTE = 1048576;

    private final CloseGuard mCloseGuard = CloseGuard.get();

    private final AsyncRenderer mRenderer;

    private RenderSpec mLastRenderSpec;

    private int mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX;
    private int mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX;

    private int mState;

    public interface OnPageContentAvailableCallback {
        public void onPageContentAvailable(BitmapDrawable content);
    }

    public PageContentRepository(Context context) {
        mRenderer = new AsyncRenderer(context);
        mState = STATE_CLOSED;
        if (DEBUG) {
            Log.i(LOG_TAG, "STATE_CLOSED");
        }
        mCloseGuard.open("destroy");
    }

    public void open(ParcelFileDescriptor source, final OpenDocumentCallback callback) {
        throwIfNotClosed();
        mState = STATE_OPENED;
        if (DEBUG) {
            Log.i(LOG_TAG, "STATE_OPENED");
        }
        mRenderer.open(source, callback);
    }

    public void close(Runnable callback) {
        throwIfNotOpened();
        mState = STATE_CLOSED;
        if (DEBUG) {
            Log.i(LOG_TAG, "STATE_CLOSED");
        }

        mRenderer.close(callback);
    }

    public void destroy(final Runnable callback) {
        if (mState == STATE_OPENED) {
            close(new Runnable() {
                @Override
                public void run() {
                    destroy(callback);
                }
            });
            return;
        }

        mState = STATE_DESTROYED;
        if (DEBUG) {
            Log.i(LOG_TAG, "STATE_DESTROYED");
        }
        mRenderer.destroy();

        if (callback != null) {
            callback.run();
        }
    }

    public void startPreload(int firstShownPage, int lastShownPage) {
        // If we do not have a render spec we have no clue what size the
        // preloaded bitmaps should be, so just take a note for what to do.
        if (mLastRenderSpec == null) {
            mScheduledPreloadFirstShownPage = firstShownPage;
            mScheduledPreloadLastShownPage = lastShownPage;
        } else if (mState == STATE_OPENED) {
            mRenderer.startPreload(firstShownPage, lastShownPage, mLastRenderSpec);
        }
    }

    public void stopPreload() {
        mRenderer.stopPreload();
    }

    public int getFilePageCount() {
        return mRenderer.getPageCount();
    }

    public PageContentProvider acquirePageContentProvider(int pageIndex, View owner) {
        throwIfDestroyed();

        if (DEBUG) {
            Log.i(LOG_TAG, "Acquiring provider for page: " + pageIndex);
        }

        return new PageContentProvider(pageIndex, owner);
    }

    public void releasePageContentProvider(PageContentProvider provider) {
        throwIfDestroyed();

        if (DEBUG) {
            Log.i(LOG_TAG, "Releasing provider for page: " + provider.mPageIndex);
        }

        provider.cancelLoad();
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (mState != STATE_DESTROYED) {
                mCloseGuard.warnIfOpen();
                destroy(null);
            }
        } finally {
            super.finalize();
        }
    }

    private void throwIfNotOpened() {
        if (mState != STATE_OPENED) {
            throw new IllegalStateException("Not opened");
        }
    }

    private void throwIfNotClosed() {
        if (mState != STATE_CLOSED) {
            throw new IllegalStateException("Not closed");
        }
    }

    private void throwIfDestroyed() {
        if (mState == STATE_DESTROYED) {
            throw new IllegalStateException("Destroyed");
        }
    }

    public final class PageContentProvider {
        private final int mPageIndex;
        private View mOwner;

        public PageContentProvider(int pageIndex, View owner) {
            mPageIndex = pageIndex;
            mOwner = owner;
        }

        public View getOwner() {
            return mOwner;
        }

        public int getPageIndex() {
            return mPageIndex;
        }

        public void getPageContent(RenderSpec renderSpec, OnPageContentAvailableCallback callback) {
            throwIfDestroyed();

            mLastRenderSpec = renderSpec;

            // We tired to preload but didn't know the bitmap size, now
            // that we know let us do the work.
            if (mScheduledPreloadFirstShownPage != INVALID_PAGE_INDEX
                    && mScheduledPreloadLastShownPage != INVALID_PAGE_INDEX) {
                startPreload(mScheduledPreloadFirstShownPage, mScheduledPreloadLastShownPage);
                mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX;
                mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX;
            }

            if (mState == STATE_OPENED) {
                mRenderer.renderPage(mPageIndex, renderSpec, callback);
            } else {
                mRenderer.getCachedPage(mPageIndex, renderSpec, callback);
            }
        }

        void cancelLoad() {
            throwIfDestroyed();

            if (mState == STATE_OPENED) {
                mRenderer.cancelRendering(mPageIndex);
            }
        }
    }

    private static final class PageContentLruCache {
        private final LinkedHashMap<Integer, RenderedPage> mRenderedPages =
                new LinkedHashMap<>();

        private final int mMaxSizeInBytes;

        private int mSizeInBytes;

        public PageContentLruCache(int maxSizeInBytes) {
            mMaxSizeInBytes = maxSizeInBytes;
        }

        public RenderedPage getRenderedPage(int pageIndex) {
            return mRenderedPages.get(pageIndex);
        }

        public RenderedPage removeRenderedPage(int pageIndex) {
            RenderedPage page = mRenderedPages.remove(pageIndex);
            if (page != null) {
                mSizeInBytes -= page.getSizeInBytes();
            }
            return page;
        }

        public RenderedPage putRenderedPage(int pageIndex, RenderedPage renderedPage) {
            RenderedPage oldRenderedPage = mRenderedPages.remove(pageIndex);
            if (oldRenderedPage != null) {
                if (!oldRenderedPage.renderSpec.equals(renderedPage.renderSpec)) {
                    throw new IllegalStateException("Wrong page size");
                }
            } else {
                final int contentSizeInBytes = renderedPage.getSizeInBytes();
                if (mSizeInBytes + contentSizeInBytes > mMaxSizeInBytes) {
                    throw new IllegalStateException("Client didn't free space");
                }

                mSizeInBytes += contentSizeInBytes;
            }
            return mRenderedPages.put(pageIndex, renderedPage);
        }

        public void invalidate() {
            for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) {
                entry.getValue().state = RenderedPage.STATE_SCRAP;
            }
        }

        public RenderedPage removeLeastNeeded() {
            if (mRenderedPages.isEmpty()) {
                return null;
            }

            // First try to remove a rendered page that holds invalidated
            // or incomplete content, i.e. its render spec is null.
            for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) {
                RenderedPage renderedPage = entry.getValue();
                if (renderedPage.state == RenderedPage.STATE_SCRAP) {
                    Integer pageIndex = entry.getKey();
                    mRenderedPages.remove(pageIndex);
                    mSizeInBytes -= renderedPage.getSizeInBytes();
                    return renderedPage;
                }
            }

            // If all rendered pages contain rendered content, then use the oldest.
            final int pageIndex = mRenderedPages.eldest().getKey();
            RenderedPage renderedPage = mRenderedPages.remove(pageIndex);
            mSizeInBytes -= renderedPage.getSizeInBytes();
            return renderedPage;
        }

        public int getSizeInBytes() {
            return mSizeInBytes;
        }

        public int getMaxSizeInBytes() {
            return mMaxSizeInBytes;
        }

        public void clear() {
            Iterator<Map.Entry<Integer, RenderedPage>> iterator =
                    mRenderedPages.entrySet().iterator();
            while (iterator.hasNext()) {
                iterator.next();
                iterator.remove();
            }
        }
    }

    public static final class RenderSpec {
        final int bitmapWidth;
        final int bitmapHeight;
        final PrintAttributes printAttributes = new PrintAttributes.Builder().build();

        public RenderSpec(int bitmapWidth, int bitmapHeight,
                MediaSize mediaSize, Margins minMargins) {
            this.bitmapWidth = bitmapWidth;
            this.bitmapHeight = bitmapHeight;
            printAttributes.setMediaSize(mediaSize);
            printAttributes.setMinMargins(minMargins);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            RenderSpec other = (RenderSpec) obj;
            if (bitmapHeight != other.bitmapHeight) {
                return false;
            }
            if (bitmapWidth != other.bitmapWidth) {
                return false;
            }
            if (printAttributes != null) {
                if (!printAttributes.equals(other.printAttributes)) {
                    return false;
                }
            } else if (other.printAttributes != null) {
                return false;
            }
            return true;
        }

        public boolean hasSameSize(RenderedPage page) {
            Bitmap bitmap = page.content.getBitmap();
            return bitmap.getWidth() == bitmapWidth
                    && bitmap.getHeight() == bitmapHeight;
        }

        @Override
        public int hashCode() {
            int result = bitmapWidth;
            result = 31 * result + bitmapHeight;
            result = 31 * result + (printAttributes != null ? printAttributes.hashCode() : 0);
            return result;
        }
    }

    private static final class RenderedPage {
        public static final int STATE_RENDERED = 0;
        public static final int STATE_RENDERING = 1;
        public static final int STATE_SCRAP = 2;

        final BitmapDrawable content;
        RenderSpec renderSpec;

        int state = STATE_SCRAP;

        RenderedPage(BitmapDrawable content) {
            this.content = content;
        }

        public int getSizeInBytes() {
            return content.getBitmap().getByteCount();
        }

        public void erase() {
            content.getBitmap().eraseColor(Color.WHITE);
        }
    }

    private static final class AsyncRenderer implements ServiceConnection {
        private final Object mLock = new Object();

        private final Context mContext;

        private final PageContentLruCache mPageContentCache;

        private final ArrayMap<Integer, RenderPageTask> mPageToRenderTaskMap = new ArrayMap<>();

        private int mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;

        @GuardedBy("mLock")
        private IPdfRenderer mRenderer;

        private OpenTask mOpenTask;

        private boolean mBoundToService;
        private boolean mDestroyed;

        public AsyncRenderer(Context context) {
            mContext = context;

            ActivityManager activityManager = (ActivityManager)
                    mContext.getSystemService(Context.ACTIVITY_SERVICE);
            final int cacheSizeInBytes = activityManager.getMemoryClass() * BYTES_PER_MEGABYTE / 4;
            mPageContentCache = new PageContentLruCache(cacheSizeInBytes);
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            synchronized (mLock) {
                mRenderer = IPdfRenderer.Stub.asInterface(service);
                mLock.notifyAll();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            synchronized (mLock) {
                mRenderer = null;
            }
        }

        public void open(ParcelFileDescriptor source, OpenDocumentCallback callback) {
            // Opening a new document invalidates the cache as it has pages
            // from the last document. We keep the cache even when the document
            // is closed to show pages while the other side is writing the new
            // document.
            mPageContentCache.invalidate();

            mOpenTask = new OpenTask(source, callback);
            mOpenTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
        }

        public void close(final Runnable callback) {
            cancelAllRendering();

            if (mOpenTask != null) {
                mOpenTask.cancel();
            }

            new AsyncTask<Void, Void, Void>() {
                @Override
                protected void onPreExecute() {
                    if (mDestroyed) {
                        cancel(true);
                        return;
                    }
                }

                @Override
                protected Void doInBackground(Void... params) {
                    synchronized (mLock) {
                        try {
                            if (mRenderer != null) {
                                mRenderer.closeDocument();
                            }
                        } catch (RemoteException re) {
                            /* ignore */
                        }
                    }
                    return null;
                }

                @Override
                public void onPostExecute(Void result) {
                    mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
                    if (callback != null) {
                        callback.run();
                    }
                }
            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
        }

        public void destroy() {
            if (mBoundToService) {
                mBoundToService = false;
                mContext.unbindService(AsyncRenderer.this);
            }

            mPageContentCache.invalidate();
            mPageContentCache.clear();
            mDestroyed = true;
        }

        public void startPreload(int firstShownPage, int lastShownPage, RenderSpec renderSpec) {
            if (DEBUG) {
                Log.i(LOG_TAG, "Preloading pages around [" + firstShownPage
                        + "-" + lastShownPage + "]");
            }

            final int bitmapSizeInBytes = renderSpec.bitmapWidth * renderSpec.bitmapHeight
                    * BYTES_PER_PIXEL;
            final int maxCachedPageCount = mPageContentCache.getMaxSizeInBytes()
                    / bitmapSizeInBytes;
            final int halfPreloadCount = (maxCachedPageCount
                    - (lastShownPage - firstShownPage)) / 2 - 1;

            final int excessFromStart;
            if (firstShownPage - halfPreloadCount < 0) {
                excessFromStart = halfPreloadCount - firstShownPage;
            } else {
                excessFromStart = 0;
            }

            final int excessFromEnd;
            if (lastShownPage + halfPreloadCount >= mPageCount) {
                excessFromEnd = (lastShownPage + halfPreloadCount) - mPageCount;
            } else {
                excessFromEnd = 0;
            }

            final int fromIndex = Math.max(firstShownPage - halfPreloadCount - excessFromEnd, 0);
            final int toIndex = Math.min(lastShownPage + halfPreloadCount + excessFromStart,
                    mPageCount - 1);

            for (int i = fromIndex; i <= toIndex; i++) {
                renderPage(i, renderSpec, null);
            }
        }

        public void stopPreload() {
            final int taskCount = mPageToRenderTaskMap.size();
            for (int i = 0; i < taskCount; i++) {
                RenderPageTask task = mPageToRenderTaskMap.valueAt(i);
                if (task.isPreload() && !task.isCancelled()) {
                    task.cancel(true);
                }
            }
        }

        public int getPageCount() {
            return mPageCount;
        }

        public void getCachedPage(int pageIndex, RenderSpec renderSpec,
                OnPageContentAvailableCallback callback) {
            RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex);
            if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED
                    && renderedPage.renderSpec.equals(renderSpec)) {
                if (DEBUG) {
                    Log.i(LOG_TAG, "Cache hit for page: " + pageIndex);
                }

                // Announce if needed.
                if (callback != null) {
                    callback.onPageContentAvailable(renderedPage.content);
                }
            }
        }

        public void renderPage(int pageIndex, RenderSpec renderSpec,
                OnPageContentAvailableCallback callback) {
            // First, check if we have a rendered page for this index.
            RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex);
            if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED) {
                // If we have rendered page with same constraints - done.
                if (renderedPage.renderSpec.equals(renderSpec)) {
                    if (DEBUG) {
                        Log.i(LOG_TAG, "Cache hit for page: " + pageIndex);
                    }

                    // Announce if needed.
                    if (callback != null) {
                        callback.onPageContentAvailable(renderedPage.content);
                    }
                    return;
                } else {
                    // If the constraints changed, mark the page obsolete.
                    renderedPage.state = RenderedPage.STATE_SCRAP;
                }
            }

            // Next, check if rendering this page is scheduled.
            RenderPageTask renderTask = mPageToRenderTaskMap.get(pageIndex);
            if (renderTask != null && !renderTask.isCancelled()) {
                // If not rendered and constraints same....
                if (renderTask.mRenderSpec.equals(renderSpec)) {
                    if (renderTask.mCallback != null) {
                        // If someone else is already waiting for this page - bad state.
                        if (callback != null && renderTask.mCallback != callback) {
                            throw new IllegalStateException("Page rendering not cancelled");
                        }
                    } else {
                        // No callback means we are preloading so just let the argument
                        // callback be attached to our work in progress.
                        renderTask.mCallback = callback;
                    }
                    return;
                } else {
                    // If not rendered and constraints changed - cancel rendering.
                    renderTask.cancel(true);
                }
            }

            // Oh well, we will have work to do...
            renderTask = new RenderPageTask(pageIndex, renderSpec, callback);
            mPageToRenderTaskMap.put(pageIndex, renderTask);
            renderTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
        }

        public void cancelRendering(int pageIndex) {
            RenderPageTask task = mPageToRenderTaskMap.get(pageIndex);
            if (task != null && !task.isCancelled()) {
                task.cancel(true);
            }
        }

        private void cancelAllRendering() {
            final int taskCount = mPageToRenderTaskMap.size();
            for (int i = 0; i < taskCount; i++) {
                RenderPageTask task = mPageToRenderTaskMap.valueAt(i);
                if (!task.isCancelled()) {
                    task.cancel(true);
                }
            }
        }

        private final class OpenTask extends AsyncTask<Void, Void, Integer> {
            private final ParcelFileDescriptor mSource;
            private final OpenDocumentCallback mCallback;

            public OpenTask(ParcelFileDescriptor source, OpenDocumentCallback callback) {
                mSource = source;
                mCallback = callback;
            }

            @Override
            protected void onPreExecute() {
                if (mDestroyed) {
                    cancel(true);
                    return;
                }
                Intent intent = new Intent(PdfManipulationService.ACTION_GET_RENDERER);
                intent.setClass(mContext, PdfManipulationService.class);
                intent.setData(Uri.fromParts("fake-scheme", String.valueOf(
                        AsyncRenderer.this.hashCode()), null));
                mContext.bindService(intent, AsyncRenderer.this, Context.BIND_AUTO_CREATE);
                mBoundToService = true;
            }

            @Override
            protected Integer doInBackground(Void... params) {
                synchronized (mLock) {
                    while (mRenderer == null && !isCancelled()) {
                        try {
                            mLock.wait();
                        } catch (InterruptedException ie) {
                                /* ignore */
                        }
                    }
                    try {
                        return mRenderer.openDocument(mSource);
                    } catch (RemoteException re) {
                        Log.e(LOG_TAG, "Cannot open PDF document");
                        return PdfManipulationService.ERROR_MALFORMED_PDF_FILE;
                    } finally {
                        // Close the fd as we passed it to another process
                        // which took ownership.
                        IoUtils.closeQuietly(mSource);
                    }
                }
            }

            @Override
            public void onPostExecute(Integer pageCount) {
                switch (pageCount) {
                    case PdfManipulationService.ERROR_MALFORMED_PDF_FILE: {
                        mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
                        if (mCallback != null) {
                            mCallback.onFailure(OpenDocumentCallback.ERROR_MALFORMED_PDF_FILE);
                        }
                    } break;
                    case PdfManipulationService.ERROR_SECURE_PDF_FILE: {
                        mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
                        if (mCallback != null) {
                            mCallback.onFailure(OpenDocumentCallback.ERROR_SECURE_PDF_FILE);
                        }
                    } break;
                    default: {
                        mPageCount = pageCount;
                        if (mCallback != null) {
                            mCallback.onSuccess();
                        }
                    } break;
                }

                mOpenTask = null;
            }

            @Override
            protected void onCancelled(Integer integer) {
                mOpenTask = null;
            }

            public void cancel() {
                cancel(true);
                synchronized(mLock) {
                    mLock.notifyAll();
                }
            }
        }

        private final class RenderPageTask extends AsyncTask<Void, Void, RenderedPage> {
            final int mPageIndex;
            final RenderSpec mRenderSpec;
            OnPageContentAvailableCallback mCallback;
            RenderedPage mRenderedPage;

            public RenderPageTask(int pageIndex, RenderSpec renderSpec,
                    OnPageContentAvailableCallback callback) {
                mPageIndex = pageIndex;
                mRenderSpec = renderSpec;
                mCallback = callback;
            }

            @Override
            protected void onPreExecute() {
                mRenderedPage = mPageContentCache.getRenderedPage(mPageIndex);
                if (mRenderedPage != null && mRenderedPage.state == RenderedPage.STATE_RENDERED) {
                    throw new IllegalStateException("Trying to render a rendered page");
                }

                // Reuse bitmap for the page only if the right size.
                if (mRenderedPage != null && !mRenderSpec.hasSameSize(mRenderedPage)) {
                    if (DEBUG) {
                        Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex
                                + " with different size.");
                    }
                    mPageContentCache.removeRenderedPage(mPageIndex);
                    mRenderedPage = null;
                }

                final int bitmapSizeInBytes = mRenderSpec.bitmapWidth
                        * mRenderSpec.bitmapHeight * BYTES_PER_PIXEL;

                // Try to find a bitmap to reuse.
                while (mRenderedPage == null) {

                    // Fill the cache greedily.
                    if (mPageContentCache.getSizeInBytes() <= 0
                            || mPageContentCache.getSizeInBytes() + bitmapSizeInBytes
                            <= mPageContentCache.getMaxSizeInBytes()) {
                        break;
                    }

                    RenderedPage renderedPage = mPageContentCache.removeLeastNeeded();

                    if (!mRenderSpec.hasSameSize(renderedPage)) {
                        if (DEBUG) {
                            Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex
                                   + " with different size.");
                        }
                        continue;
                    }

                    mRenderedPage = renderedPage;
                    renderedPage.erase();

                    if (DEBUG) {
                        Log.i(LOG_TAG, "Reused bitmap for page: " + mPageIndex + " cache size: "
                                + mPageContentCache.getSizeInBytes() + " bytes");
                    }

                    break;
                }

                if (mRenderedPage == null) {
                    if (DEBUG) {
                        Log.i(LOG_TAG, "Created bitmap for page: " + mPageIndex + " cache size: "
                                + mPageContentCache.getSizeInBytes() + " bytes");
                    }
                    Bitmap bitmap = Bitmap.createBitmap(mRenderSpec.bitmapWidth,
                            mRenderSpec.bitmapHeight, Bitmap.Config.ARGB_8888);
                    bitmap.eraseColor(Color.WHITE);
                    BitmapDrawable content = new BitmapDrawable(mContext.getResources(), bitmap);
                    mRenderedPage = new RenderedPage(content);
                }

                mRenderedPage.renderSpec = mRenderSpec;
                mRenderedPage.state = RenderedPage.STATE_RENDERING;

                mPageContentCache.putRenderedPage(mPageIndex, mRenderedPage);
            }

            @Override
            protected RenderedPage doInBackground(Void... params) {
                if (isCancelled()) {
                    return mRenderedPage;
                }

                Bitmap bitmap = mRenderedPage.content.getBitmap();

                ParcelFileDescriptor[] pipe = null;
                try {
                    pipe = ParcelFileDescriptor.createPipe();
                    ParcelFileDescriptor source = pipe[0];
                    ParcelFileDescriptor destination = pipe[1];

                    mRenderer.renderPage(mPageIndex, bitmap.getWidth(), bitmap.getHeight(),
                            mRenderSpec.printAttributes, destination);

                    // We passed the file descriptor to the other side which took
                    // ownership, so close our copy for the write to complete.
                    destination.close();

                    BitmapSerializeUtils.readBitmapPixels(bitmap, source);
                } catch (IOException|RemoteException e) {
                    Log.e(LOG_TAG, "Error rendering page:" + mPageIndex, e);
                } finally {
                    IoUtils.closeQuietly(pipe[0]);
                    IoUtils.closeQuietly(pipe[1]);
                }

                return mRenderedPage;
            }

            @Override
            public void onPostExecute(RenderedPage renderedPage) {
                if (DEBUG) {
                    Log.i(LOG_TAG, "Completed rendering page: " + mPageIndex);
                }

                // This task is done.
                mPageToRenderTaskMap.remove(mPageIndex);

                // Take a note that the content is rendered.
                renderedPage.state = RenderedPage.STATE_RENDERED;

                // Announce success if needed.
                if (mCallback != null) {
                    mCallback.onPageContentAvailable(renderedPage.content);
                }
            }

            @Override
            protected void onCancelled(RenderedPage renderedPage) {
                if (DEBUG) {
                    Log.i(LOG_TAG, "Cancelled rendering page: " + mPageIndex);
                }

                // This task is done.
                mPageToRenderTaskMap.remove(mPageIndex);

                // If canceled before on pre-execute.
                if (renderedPage == null) {
                    return;
                }

                // Take a note that the content is not rendered.
                renderedPage.state = RenderedPage.STATE_SCRAP;
            }

            public boolean isPreload() {
                return mCallback == null;
            }
        }
    }
}
