// Copyright 2013 The Chromium Authors
// 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 androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
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 the browser unless they close the dialog or press
* the 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 TAG = "printing";
/**
* 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_ONWRITE = 1;
/** Printing dialog has been dismissed and cleanup has been done. */
private static final int PRINTING_STATE_FINISHED = 2;
/** The singleton instance for this class. */
@VisibleForTesting protected static PrintingController sInstance;
private String mErrorMessage;
private PrintingContext mPrintingContext;
private int mRenderProcessId;
private int mRenderFrameId;
/** The file descriptor into which the PDF will be written. Provided by the framework. */
private ParcelFileDescriptor 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;
private boolean mIsBusy;
private PrintManagerDelegate mPrintManager;
@VisibleForTesting
protected PrintingControllerImpl() {
mPrintDocumentAdapterWrapper = new PrintDocumentAdapterWrapper();
mPrintDocumentAdapterWrapper.setPdfGenerator(this);
}
/**
* Returns the singleton instance, lazily creating one if needed.
*
* @return The singleton instance.
*/
public static PrintingController getInstance() {
ThreadUtils.assertOnUiThread();
if (sInstance == null) {
sInstance = new PrintingControllerImpl();
}
return sInstance;
}
@Override
public boolean hasPrintingFinished() {
return mPrintingState == PRINTING_STATE_FINISHED;
}
@Override
public int getDpi() {
return mDpi;
}
@Override
public int getFileDescriptor() {
return mFileDescriptor.getFd();
}
@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 PrintingContext printingContext) {
mPrintingContext = printingContext;
}
@Override
public void setPendingPrint(
final Printable printable,
PrintManagerDelegate printManager,
int renderProcessId,
int renderFrameId) {
if (mIsBusy) {
Log.d(TAG, "Pending print can't be set. PrintingController is busy.");
return;
}
mPrintable = printable;
mErrorMessage = mPrintable.getErrorMessage();
mPrintManager = printManager;
mRenderProcessId = renderProcessId;
mRenderFrameId = renderFrameId;
}
@Override
public void startPendingPrint() {
boolean canStartPrint = false;
if (mIsBusy) {
Log.d(TAG, "Pending print can't be started. PrintingController is busy.");
} else if (mPrintManager == null) {
Log.d(TAG, "Pending print can't be started. No PrintManager provided.");
} else if (!mPrintable.canPrint()) {
Log.d(TAG, "Pending print can't be started. Printable can't perform printing.");
} else {
canStartPrint = true;
}
if (!canStartPrint) return;
mIsBusy = true;
mPrintDocumentAdapterWrapper.print(mPrintManager, mPrintable.getTitle());
mPrintManager = null;
}
@Override
public void startPrint(final Printable printable, PrintManagerDelegate printManager) {
if (mIsBusy) return;
setPendingPrint(printable, printManager, mRenderProcessId, mRenderFrameId);
startPendingPrint();
}
@Override
public void pdfWritingDone(int pageCount) {
if (mPrintingState == PRINTING_STATE_READY) {
assert pageCount == 0
: "There is no pending printing task, should only be a failure report";
}
if (mPrintingState != PRINTING_STATE_STARTED_FROM_ONWRITE) return;
mPrintingState = PRINTING_STATE_READY;
closeFileDescriptor();
if (pageCount > 0) {
PageRange[] pageRanges = convertIntegerArrayToPageRanges(mPages, pageCount);
mOnWriteCallback.onWriteFinished(pageRanges);
} else {
mOnWriteCallback.onWriteFailed(mErrorMessage);
resetCallbacks();
}
}
@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();
mOnLayoutCallback = callback;
// We don't want to stack Chromium with multiple PDF generation operations before
// completion of an ongoing one.
if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONWRITE) {
callback.onLayoutFailed(mErrorMessage);
resetCallbacks();
} else {
PrintDocumentInfo info =
new PrintDocumentInfo.Builder(mPrintable.getTitle())
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
// Set page count to unknown since Android framework will get it from
// PDF file generated in onWrite.
.setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
.build();
// We always need to generate a new PDF. onLayout is not only called when attributes
// changed, but also when pages need to print got selected. We can't tell if the later
// case was happened, so has to generate a new file.
mOnLayoutCallback.onLayoutFinished(info, true);
}
}
@Override
public void onWrite(
final PageRange[] ranges,
final ParcelFileDescriptor destination,
final CancellationSignal cancellationSignal,
final PrintDocumentAdapterWrapper.WriteResultCallbackWrapper callback) {
// TODO(cimamoglu): Make use of CancellationSignal.
if (ranges == null || ranges.length == 0) {
callback.onWriteFailed(null);
return;
}
mOnWriteCallback = callback;
assert mPrintingState == PRINTING_STATE_READY;
assert mFileDescriptor == null;
try {
mFileDescriptor = destination.dup();
} catch (IOException e) {
mOnWriteCallback.onWriteFailed("ParcelFileDescriptor.dup() failed: " + e.toString());
resetCallbacks();
return;
}
mPages = convertPageRangesToIntegerArray(ranges);
// mRenderProcessId and mRenderFrameId could be invalid values, in this case we are going to
// print the main frame.
if (mPrintable.print(mRenderProcessId, mRenderFrameId)) {
mPrintingState = PRINTING_STATE_STARTED_FROM_ONWRITE;
} else {
mOnWriteCallback.onWriteFailed(mErrorMessage);
resetCallbacks();
}
// 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() {
mPages = null;
mPrintingContext = null;
mRenderProcessId = -1;
mRenderFrameId = -1;
mPrintingState = PRINTING_STATE_FINISHED;
closeFileDescriptor();
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 void closeFileDescriptor() {
if (mFileDescriptor == null) return;
try {
mFileDescriptor.close();
} catch (IOException ioe) {
/* ignore */
} finally {
mFileDescriptor = null;
}
}
private static PageRange[] convertIntegerArrayToPageRanges(int[] pagesArray, int pageCount) {
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[] {new PageRange(0, pageCount - 1)};
}
return pageRanges;
}
/** Gets an array of page ranges and returns an array of integers with all ranges expanded. */
private static int[] convertPageRangesToIntegerArray(final PageRange[] ranges) {
if (ranges.length == 1 && ranges[0].equals(PageRange.ALL_PAGES)) {
// null corresponds to all pages in Chromium printing logic.
return null;
}
// 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;
}
}