root/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. create
  2. getInstance
  3. hasPrintingFinished
  4. getDpi
  5. getFileDescriptor
  6. getPageHeight
  7. getPageWidth
  8. getPageNumbers
  9. isBusy
  10. setPrintingContext
  11. startPrint
  12. pdfWritingDone
  13. onStart
  14. onLayout
  15. pageCountEstimationDone
  16. onWrite
  17. onFinish
  18. resetCallbacks
  19. closeFileDescriptor
  20. convertIntegerArrayToPageRanges
  21. normalizeRanges

// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.printing;

import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentInfo;

import org.chromium.base.ThreadUtils;
import org.chromium.printing.PrintDocumentAdapterWrapper.PdfGenerator;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * Controls the interactions with Android framework related to printing.
 *
 * This class is singleton, since at any point at most one printing dialog can exist. Also, since
 * this dialog is modal, user can't interact with browser unless s/he closes the dialog or presses
 * print button. The singleton object lives in UI thread. Interaction with the native side is
 * carried through PrintingContext class.
 */
public class PrintingControllerImpl implements PrintingController, PdfGenerator {

    private static final String LOG_TAG = "PrintingControllerImpl";

    /**
     * This is used for both initial state and a completed state (i.e. starting from either
     * onLayout or onWrite, a PDF generation cycle is completed another new one can safely start).
     */
    private static final int PRINTING_STATE_READY = 0;
    private static final int PRINTING_STATE_STARTED_FROM_ONLAYOUT = 1;
    private static final int PRINTING_STATE_STARTED_FROM_ONWRITE = 2;
    /** Printing dialog has been dismissed and cleanup has been done. */
    private static final int PRINTING_STATE_FINISHED = 3;

    /** The singleton instance for this class. */
    private static PrintingController sInstance;

    private final String mErrorMessage;

    private PrintingContextInterface mPrintingContext;

    /** The file descriptor into which the PDF will be written.  Provided by the framework. */
    private int mFileDescriptor;

    /** Dots per inch, as provided by the framework. */
    private int mDpi;

    /** Paper dimensions. */
    private PrintAttributes.MediaSize mMediaSize;

    /** Numbers of pages to be printed, zero indexed. */
    private int[] mPages;

    /** The callback function to inform the result of PDF generation to the framework. */
    private PrintDocumentAdapterWrapper.WriteResultCallbackWrapper mOnWriteCallback;

    /**
     * The callback function to inform the result of layout to the framework.  We save the callback
     * because we start the native PDF generation process inside onLayout, and we need to pass the
     * number of expected pages back to the framework through this callback once the native side
     * has that information.
     */
    private PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper mOnLayoutCallback;

    /** The object through which native PDF generation process is initiated. */
    private Printable mPrintable;

    /** The object through which the framework will make calls for generating PDF. */
    private PrintDocumentAdapterWrapper mPrintDocumentAdapterWrapper;

    private int mPrintingState = PRINTING_STATE_READY;

    /** Whether layouting parameters have been changed to require a new PDF generation. */
    private boolean mNeedNewPdf = false;

    /** Total number of pages to print with initial print dialog settings. */
    private int mLastKnownMaxPages = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;

    private boolean mIsBusy = false;

    private PrintingControllerImpl(PrintDocumentAdapterWrapper printDocumentAdapterWrapper,
                                   String errorText) {
        mErrorMessage = errorText;
        mPrintDocumentAdapterWrapper = printDocumentAdapterWrapper;
        mPrintDocumentAdapterWrapper.setPdfGenerator(this);
    }

    /**
     * Creates a controller for handling printing with the framework.
     *
     * The controller is a singleton, since there can be only one printing action at any time.
     *
     * @param printDocumentAdapterWrapper The object through which the framework will make calls
     *                                    for generating PDF.
     * @param errorText The error message to be shown to user in case something goes wrong in PDF
     *                  generation in Chromium. We pass it here as a string so src/printing/android
     *                  doesn't need any string dependency.
     * @return The resulting PrintingController.
     */
    public static PrintingController create(
            PrintDocumentAdapterWrapper printDocumentAdapterWrapper, String errorText) {
        ThreadUtils.assertOnUiThread();

        if (sInstance == null) {
            sInstance = new PrintingControllerImpl(printDocumentAdapterWrapper, errorText);
        }
        return sInstance;
    }

    /**
     * Returns the singleton instance, created by the {@link PrintingControllerImpl#create}.
     *
     * This method must be called once {@link PrintingControllerImpl#create} is called, and always
     * thereafter.
     *
     * @return The singleton instance.
     */
    public static PrintingController getInstance() {
        return sInstance;
    }

    @Override
    public boolean hasPrintingFinished() {
      return mPrintingState == PRINTING_STATE_FINISHED;
    }

    @Override
    public int getDpi() {
        return mDpi;
    }

    @Override
    public int getFileDescriptor() {
        return mFileDescriptor;
    }

    @Override
    public int getPageHeight() {
        return mMediaSize.getHeightMils();
    }

    @Override
    public int getPageWidth() {
        return mMediaSize.getWidthMils();
    }

    @Override
    public int[] getPageNumbers() {
        return mPages == null ? null : mPages.clone();
    }

    @Override
    public boolean isBusy() {
        return mIsBusy;
    }

    @Override
    public void setPrintingContext(final PrintingContextInterface printingContext) {
        mPrintingContext = printingContext;
    }

    @Override
    public void startPrint(final Printable printable, PrintManagerDelegate printManager) {
        if (mIsBusy) return;
        mIsBusy = true;
        mPrintable = printable;
        mPrintDocumentAdapterWrapper.print(printManager, printable.getTitle());
    }

    @Override
    public void pdfWritingDone(boolean success) {
        if (mPrintingState == PRINTING_STATE_FINISHED) return;
        mPrintingState = PRINTING_STATE_READY;
        if (success) {
            PageRange[] pageRanges = convertIntegerArrayToPageRanges(mPages);
            mOnWriteCallback.onWriteFinished(pageRanges);
        } else {
            mOnWriteCallback.onWriteFailed(mErrorMessage);
            resetCallbacks();
        }
        closeFileDescriptor(mFileDescriptor);
        mFileDescriptor = -1;
    }

    @Override
    public void onStart() {
        mPrintingState = PRINTING_STATE_READY;
    }

    @Override
    public void onLayout(
            PrintAttributes oldAttributes,
            PrintAttributes newAttributes,
            CancellationSignal cancellationSignal,
            PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper callback,
            Bundle metadata) {
        // NOTE: Chrome printing just supports one DPI, whereas Android has both vertical and
        // horizontal.  These two values are most of the time same, so we just pass one of them.
        mDpi = newAttributes.getResolution().getHorizontalDpi();
        mMediaSize = newAttributes.getMediaSize();

        mNeedNewPdf = !newAttributes.equals(oldAttributes);

        mOnLayoutCallback = callback;
        // We don't want to stack Chromium with multiple PDF generation operations before
        // completion of an ongoing one.
        // TODO(cimamoglu): Whenever onLayout is called, generate a new PDF with the new
        //                  parameters. Hence, we can get the true number of pages.
        if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
            // We don't start a new Chromium PDF generation operation if there's an existing
            // onLayout going on. Use the last known valid page count.
            pageCountEstimationDone(mLastKnownMaxPages);
        } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONWRITE) {
            callback.onLayoutFailed(mErrorMessage);
            resetCallbacks();
        } else if (mPrintable.print()) {
            mPrintingState = PRINTING_STATE_STARTED_FROM_ONLAYOUT;
        } else {
            callback.onLayoutFailed(mErrorMessage);
            resetCallbacks();
        }
    }

    @Override
    public void pageCountEstimationDone(final int maxPages) {
        // This method might be called even after onFinish, e.g. as a result of a long page
        // estimation operation.  We make sure that such call has no effect, since the printing
        // dialog has already been dismissed and relevant cleanup has already been done.
        // Also, this ensures that we do not call askUserForSettingsReply twice.
        if (mPrintingState == PRINTING_STATE_FINISHED) return;
        if (maxPages != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
            mLastKnownMaxPages = maxPages;
        }
        if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
            PrintDocumentInfo info = new PrintDocumentInfo.Builder(mPrintable.getTitle())
                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                    .setPageCount(mLastKnownMaxPages)
                    .build();
            mOnLayoutCallback.onLayoutFinished(info, mNeedNewPdf);
        } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONWRITE) {
            // Chromium PDF generation is started inside onWrite, continue that.
            if (mPrintingContext == null) {
                mOnWriteCallback.onWriteFailed(mErrorMessage);
                resetCallbacks();
                return;
            }
            mPrintingContext.askUserForSettingsReply(true);
        }
    }

    @Override
    public void onWrite(
            final PageRange[] ranges,
            final ParcelFileDescriptor destination,
            final CancellationSignal cancellationSignal,
            final PrintDocumentAdapterWrapper.WriteResultCallbackWrapper callback) {
        if (mPrintingContext == null) {
            callback.onWriteFailed(mErrorMessage);
            resetCallbacks();
            return;
        }

        // TODO(cimamoglu): Make use of CancellationSignal.
        mOnWriteCallback = callback;

        mFileDescriptor = destination.getFd();
        // Update file descriptor to PrintingContext mapping in the owner class.
        mPrintingContext.updatePrintingContextMap(mFileDescriptor, false);

        // We need to convert ranges list into an array of individual numbers for
        // easier JNI passing and compatibility with the native side.
        if (ranges.length == 1 && ranges[0].equals(PageRange.ALL_PAGES)) {
            // null corresponds to all pages in Chromium printing logic.
            mPages = null;
        } else {
            mPages = normalizeRanges(ranges);
        }

        if (mPrintingState == PRINTING_STATE_READY) {
            // If this onWrite is without a preceding onLayout, start Chromium PDF generation here.
            if (mPrintable.print()) {
                mPrintingState = PRINTING_STATE_STARTED_FROM_ONWRITE;
            } else {
                callback.onWriteFailed(mErrorMessage);
                resetCallbacks();
            }
        } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
            // Otherwise, continue previously started operation.
            mPrintingContext.askUserForSettingsReply(true);
        }
        // We are guaranteed by the framework that we will not have two onWrite calls at once.
        // We may get a CancellationSignal, after replying it (via WriteResultCallback) we might
        // get another onWrite call.
    }

    @Override
    public void onFinish() {
        mLastKnownMaxPages = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
        mPages = null;

        if (mPrintingContext != null) {
            if (mPrintingState != PRINTING_STATE_READY) {
                // Note that we are never making an extraneous askUserForSettingsReply call.
                // If we are in the middle of a PDF generation from onLayout or onWrite, it means
                // the state isn't PRINTING_STATE_READY, so we enter here and make this call (no
                // extra). If we complete the PDF generation successfully from onLayout or onWrite,
                // we already make the state PRINTING_STATE_READY and call askUserForSettingsReply
                // inside pdfWritingDone, thus not entering here.  Also, if we get an extra
                // AskUserForSettings call, it's handled inside {@link
                // PrintingContext#pageCountEstimationDone}.
                mPrintingContext.askUserForSettingsReply(false);
            }
            mPrintingContext.updatePrintingContextMap(mFileDescriptor, true);
            mPrintingContext = null;
        }
        mPrintingState = PRINTING_STATE_FINISHED;

        closeFileDescriptor(mFileDescriptor);
        mFileDescriptor = -1;

        resetCallbacks();
        // The printmanager contract is that onFinish() is always called as the last
        // callback. We set busy to false here.
        mIsBusy = false;
    }

    private void resetCallbacks() {
        mOnWriteCallback = null;
        mOnLayoutCallback = null;
    }

    private static void closeFileDescriptor(int fd) {
        if (fd != -1) return;
        ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.adoptFd(fd);
        if (fileDescriptor != null) {
            try {
                fileDescriptor.close();
            } catch (IOException ioe) {
                /* ignore */
            }
        }
    }

    private static PageRange[] convertIntegerArrayToPageRanges(int[] pagesArray) {
        PageRange[] pageRanges;
        if (pagesArray != null) {
            pageRanges = new PageRange[pagesArray.length];
            for (int i = 0; i < pageRanges.length; i++) {
                int page = pagesArray[i];
                pageRanges[i] = new PageRange(page, page);
            }
        } else {
            // null corresponds to all pages in Chromium printing logic.
            pageRanges = new PageRange[] { PageRange.ALL_PAGES };
        }
        return pageRanges;
    }

    /**
     * Gets an array of page ranges and returns an array of integers with all ranges expanded.
     */
    private static int[] normalizeRanges(final PageRange[] ranges) {
        // Expand ranges into a list of individual numbers.
        ArrayList<Integer> pages = new ArrayList<Integer>();
        for (PageRange range : ranges) {
            for (int i = range.getStart(); i <= range.getEnd(); i++) {
                pages.add(i);
            }
        }

        // Convert the list into array.
        int[] ret = new int[pages.size()];
        Iterator<Integer> iterator = pages.iterator();
        for (int i = 0; i < ret.length; i++) {
            ret[i] = iterator.next().intValue();
        }
        return ret;
    }
}

/* [<][>][^][v][top][bottom][index][help] */