diff options
author | cimamoglu@chromium.org <cimamoglu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-11-14 17:29:24 +0000 |
---|---|---|
committer | cimamoglu@chromium.org <cimamoglu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-11-14 17:29:24 +0000 |
commit | b6f76a1ae51b531ef1daabd9bd05fd8c87e61cd4 (patch) | |
tree | 911fce788076849f876b6fcb2a297edecd053451 /printing/android | |
parent | b50d4e7248ebd524b7a1e07b4cdf232ec54f0f1e (diff) | |
download | chromium_src-b6f76a1ae51b531ef1daabd9bd05fd8c87e61cd4.zip chromium_src-b6f76a1ae51b531ef1daabd9bd05fd8c87e61cd4.tar.gz chromium_src-b6f76a1ae51b531ef1daabd9bd05fd8c87e61cd4.tar.bz2 |
Upstream Android printing code.
With regards to the original downstream code, there are some differences:
* PrintintControllerComponent(Impl) are renamed to PrintingController(Impl)
* ChromeComponentFactory is renamed into PrintingControllerFactory
* Some obsolete documentation removed/updated.
More refactoring and tests will follow.
BUG=315229
TBR=vitalybuka@chromium.org
Review URL: https://codereview.chromium.org/64093004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@235171 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'printing/android')
6 files changed, 671 insertions, 0 deletions
diff --git a/printing/android/java/src/org/chromium/printing/Printable.java b/printing/android/java/src/org/chromium/printing/Printable.java new file mode 100644 index 0000000..25c0ab2 --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/Printable.java @@ -0,0 +1,18 @@ +// 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; + +/** + * Describes a class that can initiate the printing process. + * + * This interface helps decoupling Tab from the printing implementation and helps with testing. + */ +public interface Printable { + /** Start the PDF generation process. */ + boolean print(); + + /** Get the title of the generated PDF document. */ + String getTitle(); +} diff --git a/printing/android/java/src/org/chromium/printing/PrintingContext.java b/printing/android/java/src/org/chromium/printing/PrintingContext.java new file mode 100644 index 0000000..e63c978 --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/PrintingContext.java @@ -0,0 +1,162 @@ +// 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 org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; + +import android.content.Context; + +import android.util.Log; +import android.util.SparseArray; + +/** + * This class is responsible for communicating with its native counterpart through JNI to handle + * the generation of PDF. On the Java side, it works with a {@link PrintingController} + * to talk to the framework. + */ +@JNINamespace("printing") +public class PrintingContext implements PrintingContextInterface { + + private static final String TAG = "PrintingContext"; + + /** Whether the framework supports printing. */ + public static final boolean sIsPrintingAvailable = isPrintingAvailable(); + + /** + * The full class name of the print manager used to test whether printing functionality is + * available. + */ + private static final String PRINT_MANAGER_CLASS_NAME = "android.print.PrintManager"; + + /** + * Mapping from a file descriptor (as originally provided from + * {@link PrintDocumentAdapter#onWrite}) to a PrintingContext. + * + * This is static because a static method of the native code (inside PrintingContextAndroid) + * needs to find Java PrintingContext class corresponding to a file descriptor. + **/ + private static final SparseArray<PrintingContext> sPrintingContextMap = + new SparseArray<PrintingContext>(); + + /** The controller this object interacts with, which in turn communicates with the framework. */ + private final PrintingController mController; + + /** The pointer to the native PrintingContextAndroid object. */ + private final int mNativeObject; + + private PrintingContext(Context context, int ptr) { + mController = PrintingControllerFactory.getPrintingController(context); + mNativeObject = ptr; + } + + /** + * @return Whether printing is supported by the platform. + */ + private static boolean isPrintingAvailable() { + // TODO(cimamoglu): Get rid of reflection once Build.VERSION_CODES.KEY_LIME_PIE is fixed. + try { + Class.forName(PRINT_MANAGER_CLASS_NAME); + } catch (ClassNotFoundException e) { + Log.d(TAG, "PrintManager not found on device"); + return false; + } + return true; + } + + /** + * Updates sPrintingContextMap to map from the file descriptor to this object. + * @param fileDescriptor The file descriptor passed down from + * {@link PrintDocumentAdapter#onWrite}. + * @param delete If true, delete the entry (if it exists). If false, add it to the map. + */ + @Override + public void updatePrintingContextMap(int fileDescriptor, boolean delete) { + ThreadUtils.assertOnUiThread(); + if (delete) { + sPrintingContextMap.remove(fileDescriptor); + } else { + sPrintingContextMap.put(fileDescriptor, this); + } + } + + /** + * Notifies the native side that the user just chose a new set of printing settings. + * @param success True if the user has chosen printing settings necessary for the + * generation of PDF, false if there has been a problem. + */ + @Override + public void askUserForSettingsReply(boolean success) { + nativeAskUserForSettingsReply(mNativeObject, success); + } + + @CalledByNative + public static PrintingContext create(Context context, int nativeObjectPointer) { + ThreadUtils.assertOnUiThread(); + return new PrintingContext(context, nativeObjectPointer); + } + + @CalledByNative + public int getFileDescriptor() { + ThreadUtils.assertOnUiThread(); + return mController.getFileDescriptor(); + } + + @CalledByNative + public int getDpi() { + ThreadUtils.assertOnUiThread(); + return mController.getDpi(); + } + + @CalledByNative + public int getWidth() { + ThreadUtils.assertOnUiThread(); + return mController.getPageWidth(); + } + + @CalledByNative + public int getHeight() { + ThreadUtils.assertOnUiThread(); + return mController.getPageHeight(); + } + + @CalledByNative + public static void pdfWritingDone(int fd, boolean success) { + ThreadUtils.assertOnUiThread(); + // TODO(cimamoglu): Do something when fd == -1. + if (sPrintingContextMap.get(fd) != null) { + ThreadUtils.assertOnUiThread(); + PrintingContext printingContext = sPrintingContextMap.get(fd); + printingContext.mController.pdfWritingDone(success); + sPrintingContextMap.remove(fd); + } + } + + @CalledByNative + public int[] getPages() { + ThreadUtils.assertOnUiThread(); + return mController.getPageNumbers(); + } + + @CalledByNative + public void pageCountEstimationDone(final int maxPages) { + ThreadUtils.assertOnUiThread(); + // If the printing dialog has already finished, tell Chromium that operation is cancelled. + if (mController.hasPrintingFinished()) { + // NOTE: We don't call nativeAskUserForSettingsReply (hence Chromium callback in + // AskUserForSettings callback) twice. See PrintingControllerImpl#onFinish + // for more explanation. + nativeAskUserForSettingsReply(mNativeObject, false); + } else { + mController.setPrintingContext(this); + mController.pageCountEstimationDone(maxPages); + } + } + + private native void nativeAskUserForSettingsReply( + int nativePrintingContextAndroid, + boolean success); +}
\ No newline at end of file diff --git a/printing/android/java/src/org/chromium/printing/PrintingContextInterface.java b/printing/android/java/src/org/chromium/printing/PrintingContextInterface.java new file mode 100644 index 0000000..80369b3 --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/PrintingContextInterface.java @@ -0,0 +1,24 @@ +// 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; + +/** + * Defines an interface for PrintingContext. + */ +public interface PrintingContextInterface { + /** + * Updates file descriptor to class instance mapping. + * @param fileDescriptor The file descriptor to which the current PrintingContext will be + * mapped. + * @param delete If true, delete the entry (if it exists). If false, add it to the map. + */ + void updatePrintingContextMap(int fileDescriptor, boolean delete); + + /** + * Notifies the native side if the printing settings are successfully prepared. + * @param success True if the settings are successfully prepared to be used by the native side. + */ + void askUserForSettingsReply(boolean success); +} diff --git a/printing/android/java/src/org/chromium/printing/PrintingController.java b/printing/android/java/src/org/chromium/printing/PrintingController.java new file mode 100644 index 0000000..beed86b --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/PrintingController.java @@ -0,0 +1,85 @@ +// 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; + +/** + * This interface describes a class which is responsible of talking to the printing backend. + * + * Such class communicates with a {@link PrintingContext}, which in turn talks to the native side. + */ +public interface PrintingController { + /** + * @return Dots Per Inch (DPI) of the currently selected printer. + */ + int getDpi(); + + /** + * @return The file descriptor number of the file into which Chromium will write the PDF. This + * is provided to us by {@link PrintDocumentAdapter#onWrite}. + */ + int getFileDescriptor(); + + /** + * @return The media height in mils (thousands of an inch). + */ + int getPageHeight(); + + /** + * @return The media width in mils (thousands of an inch). + */ + int getPageWidth(); + + /** + * @return The individual page numbers of the document to be printed, of null if all pages are + * to be printed. The numbers are zero indexed. + */ + int[] getPageNumbers(); + + /** + * Initiates the printing process for the Android API. + * + * @param printable An object capable of starting native side PDF generation, i.e. typically + * a Tab. + */ + void startPrint(final Printable printable); + + /** + * This method is called by the native side to signal PDF writing process is completed. + * + * @param success Whether the PDF is written into the provided file descriptor successfully. + */ + void pdfWritingDone(boolean success); + + /** + * Called when the native side estimates the number of pages in the PDF (before generation). + * + * @param maxPages Number of pages in the PDF, according to the last provided settings. + * If this is PrintDocumentInfo.PAGE_COUNT_UNKNOWN, then use the last known + * valid max pages count. + */ + void pageCountEstimationDone(final int maxPages); + + /** + * Sets PrintingContext. + * + * This needs to be called after PrintingContext object is created. Firstly its native + * counterpart is created, and then the Java. PrintingController implementation + * needs this to interact with the native side, since JNI is built on PrintingContext. + **/ + void setPrintingContext(final PrintingContextInterface printingContext); + + /** + * TODO(cimamoglu): Remove errorText stuff once KitKat is public and we can move this code. + * @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 because this folder + * cannot use resources directly (or any other Clank code). + */ + void setErrorText(final String errorText); + + /** + * @return Whether a complete PDF generation cycle inside Chromium has been completed. + */ + boolean hasPrintingFinished(); +} diff --git a/printing/android/java/src/org/chromium/printing/PrintingControllerFactory.java b/printing/android/java/src/org/chromium/printing/PrintingControllerFactory.java new file mode 100644 index 0000000..78c67db --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/PrintingControllerFactory.java @@ -0,0 +1,33 @@ +// 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 org.chromium.base.ThreadUtils; + +import android.content.Context; + +public class PrintingControllerFactory { + /** The singleton instance for this class. */ + private static PrintingController sInstance; + + private PrintingControllerFactory() {} // Static factory + + /** + * 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 context The application context. + * @return The resulting PrintingController. + */ + public static PrintingController getPrintingController( + Context context) { + ThreadUtils.assertOnUiThread(); + if (sInstance == null) { + sInstance = new PrintingControllerImpl(context); + } + return sInstance; + } +} diff --git a/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java b/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java new file mode 100644 index 0000000..34c4dc4 --- /dev/null +++ b/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java @@ -0,0 +1,349 @@ +// 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.content.Context; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.CancellationSignal.OnCancelListener; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Resolution; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentAdapter.LayoutResultCallback; +import android.print.PrintDocumentAdapter.WriteResultCallback; +import android.print.PrintDocumentInfo; +import android.print.PrintJob; +import android.print.PrintManager; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * 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. + */ +class PrintingControllerImpl extends PrintDocumentAdapter implements PrintingController { + + private static final String LOG_TAG = "PrintingControllerImpl"; + + private static final String PDF_FILE_NAME = "chrome_print_document.pdf"; + + private String mErrorMessage; + + private final PrintManager mPrintManager; + + 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 PrintDocumentAdapter.WriteResultCallback 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 PrintDocumentAdapter.LayoutResultCallback mOnLayoutCallback; + + /** The object through which native PDF generation process is initiated. */ + private Printable mPrintable; + + /** + * 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 final static int PRINTING_STATE_READY = 0; + private final static int PRINTING_STATE_STARTED_FROM_ONLAYOUT = 1; + private final static int PRINTING_STATE_STARTED_FROM_ONWRITE = 2; + /** Printing dialog has been dismissed and cleanup has been done. */ + private final static int PRINTING_STATE_FINISHED = 3; + + 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; + + PrintingControllerImpl(final Context context) { + mPrintManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE); + } + + @Override + public boolean hasPrintingFinished() { + return mPrintingState == PRINTING_STATE_FINISHED; + } + + @Override + public void setErrorText(final String errorText) { + mErrorMessage = errorText; + } + + @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; + } + + @Override + public void setPrintingContext(final PrintingContextInterface printingContext) { + mPrintingContext = printingContext; + } + + @Override + public void startPrint(final Printable printable) { + mPrintable = printable; + mPrintManager.print(printable.getTitle(), this, null); + } + + @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, + LayoutResultCallback 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) { + // TODO(cimamoglu): Choose a meaningful filename. + PrintDocumentInfo info = new PrintDocumentInfo.Builder(PDF_FILE_NAME) + .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 WriteResultCallback 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() { + super.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(); + } + + private void resetCallbacks() { + mOnWriteCallback = null; + mOnLayoutCallback = null; + } + + private void closeFileDescriptor(int fd) { + if (fd != -1) return; + ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.adoptFd(fd); + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + + private 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 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; + } +} |