• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.printing;
6 
7 import android.os.Bundle;
8 import android.os.CancellationSignal;
9 import android.os.ParcelFileDescriptor;
10 import android.print.PageRange;
11 import android.print.PrintAttributes;
12 import android.print.PrintDocumentInfo;
13 
14 import org.chromium.base.ThreadUtils;
15 import org.chromium.printing.PrintDocumentAdapterWrapper.PdfGenerator;
16 
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.Iterator;
20 
21 /**
22  * Controls the interactions with Android framework related to printing.
23  *
24  * This class is singleton, since at any point at most one printing dialog can exist. Also, since
25  * this dialog is modal, user can't interact with browser unless s/he closes the dialog or presses
26  * print button. The singleton object lives in UI thread. Interaction with the native side is
27  * carried through PrintingContext class.
28  */
29 public class PrintingControllerImpl implements PrintingController, PdfGenerator {
30 
31     private static final String LOG_TAG = "PrintingControllerImpl";
32 
33     /**
34      * This is used for both initial state and a completed state (i.e. starting from either
35      * onLayout or onWrite, a PDF generation cycle is completed another new one can safely start).
36      */
37     private static final int PRINTING_STATE_READY = 0;
38     private static final int PRINTING_STATE_STARTED_FROM_ONLAYOUT = 1;
39     private static final int PRINTING_STATE_STARTED_FROM_ONWRITE = 2;
40     /** Printing dialog has been dismissed and cleanup has been done. */
41     private static final int PRINTING_STATE_FINISHED = 3;
42 
43     /** The singleton instance for this class. */
44     private static PrintingController sInstance;
45 
46     private final String mErrorMessage;
47 
48     private PrintingContextInterface mPrintingContext;
49 
50     /** The file descriptor into which the PDF will be written.  Provided by the framework. */
51     private int mFileDescriptor;
52 
53     /** Dots per inch, as provided by the framework. */
54     private int mDpi;
55 
56     /** Paper dimensions. */
57     private PrintAttributes.MediaSize mMediaSize;
58 
59     /** Numbers of pages to be printed, zero indexed. */
60     private int[] mPages;
61 
62     /** The callback function to inform the result of PDF generation to the framework. */
63     private PrintDocumentAdapterWrapper.WriteResultCallbackWrapper mOnWriteCallback;
64 
65     /**
66      * The callback function to inform the result of layout to the framework.  We save the callback
67      * because we start the native PDF generation process inside onLayout, and we need to pass the
68      * number of expected pages back to the framework through this callback once the native side
69      * has that information.
70      */
71     private PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper mOnLayoutCallback;
72 
73     /** The object through which native PDF generation process is initiated. */
74     private Printable mPrintable;
75 
76     /** The object through which the framework will make calls for generating PDF. */
77     private PrintDocumentAdapterWrapper mPrintDocumentAdapterWrapper;
78 
79     private int mPrintingState = PRINTING_STATE_READY;
80 
81     /** Whether layouting parameters have been changed to require a new PDF generation. */
82     private boolean mNeedNewPdf = false;
83 
84     /** Total number of pages to print with initial print dialog settings. */
85     private int mLastKnownMaxPages = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
86 
87     private boolean mIsBusy = false;
88 
PrintingControllerImpl(PrintDocumentAdapterWrapper printDocumentAdapterWrapper, String errorText)89     private PrintingControllerImpl(PrintDocumentAdapterWrapper printDocumentAdapterWrapper,
90                                    String errorText) {
91         mErrorMessage = errorText;
92         mPrintDocumentAdapterWrapper = printDocumentAdapterWrapper;
93         mPrintDocumentAdapterWrapper.setPdfGenerator(this);
94     }
95 
96     /**
97      * Creates a controller for handling printing with the framework.
98      *
99      * The controller is a singleton, since there can be only one printing action at any time.
100      *
101      * @param printDocumentAdapterWrapper The object through which the framework will make calls
102      *                                    for generating PDF.
103      * @param errorText The error message to be shown to user in case something goes wrong in PDF
104      *                  generation in Chromium. We pass it here as a string so src/printing/android
105      *                  doesn't need any string dependency.
106      * @return The resulting PrintingController.
107      */
create( PrintDocumentAdapterWrapper printDocumentAdapterWrapper, String errorText)108     public static PrintingController create(
109             PrintDocumentAdapterWrapper printDocumentAdapterWrapper, String errorText) {
110         ThreadUtils.assertOnUiThread();
111 
112         if (sInstance == null) {
113             sInstance = new PrintingControllerImpl(printDocumentAdapterWrapper, errorText);
114         }
115         return sInstance;
116     }
117 
118     /**
119      * Returns the singleton instance, created by the {@link PrintingControllerImpl#create}.
120      *
121      * This method must be called once {@link PrintingControllerImpl#create} is called, and always
122      * thereafter.
123      *
124      * @return The singleton instance.
125      */
getInstance()126     public static PrintingController getInstance() {
127         return sInstance;
128     }
129 
130     @Override
hasPrintingFinished()131     public boolean hasPrintingFinished() {
132       return mPrintingState == PRINTING_STATE_FINISHED;
133     }
134 
135     @Override
getDpi()136     public int getDpi() {
137         return mDpi;
138     }
139 
140     @Override
getFileDescriptor()141     public int getFileDescriptor() {
142         return mFileDescriptor;
143     }
144 
145     @Override
getPageHeight()146     public int getPageHeight() {
147         return mMediaSize.getHeightMils();
148     }
149 
150     @Override
getPageWidth()151     public int getPageWidth() {
152         return mMediaSize.getWidthMils();
153     }
154 
155     @Override
getPageNumbers()156     public int[] getPageNumbers() {
157         return mPages == null ? null : mPages.clone();
158     }
159 
160     @Override
isBusy()161     public boolean isBusy() {
162         return mIsBusy;
163     }
164 
165     @Override
setPrintingContext(final PrintingContextInterface printingContext)166     public void setPrintingContext(final PrintingContextInterface printingContext) {
167         mPrintingContext = printingContext;
168     }
169 
170     @Override
startPrint(final Printable printable, PrintManagerDelegate printManager)171     public void startPrint(final Printable printable, PrintManagerDelegate printManager) {
172         if (mIsBusy) return;
173         mIsBusy = true;
174         mPrintable = printable;
175         mPrintDocumentAdapterWrapper.print(printManager, printable.getTitle());
176     }
177 
178     @Override
pdfWritingDone(boolean success)179     public void pdfWritingDone(boolean success) {
180         if (mPrintingState == PRINTING_STATE_FINISHED) return;
181         mPrintingState = PRINTING_STATE_READY;
182         if (success) {
183             PageRange[] pageRanges = convertIntegerArrayToPageRanges(mPages);
184             mOnWriteCallback.onWriteFinished(pageRanges);
185         } else {
186             mOnWriteCallback.onWriteFailed(mErrorMessage);
187             resetCallbacks();
188         }
189         closeFileDescriptor(mFileDescriptor);
190         mFileDescriptor = -1;
191     }
192 
193     @Override
onStart()194     public void onStart() {
195         mPrintingState = PRINTING_STATE_READY;
196     }
197 
198     @Override
onLayout( PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper callback, Bundle metadata)199     public void onLayout(
200             PrintAttributes oldAttributes,
201             PrintAttributes newAttributes,
202             CancellationSignal cancellationSignal,
203             PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper callback,
204             Bundle metadata) {
205         // NOTE: Chrome printing just supports one DPI, whereas Android has both vertical and
206         // horizontal.  These two values are most of the time same, so we just pass one of them.
207         mDpi = newAttributes.getResolution().getHorizontalDpi();
208         mMediaSize = newAttributes.getMediaSize();
209 
210         mNeedNewPdf = !newAttributes.equals(oldAttributes);
211 
212         mOnLayoutCallback = callback;
213         // We don't want to stack Chromium with multiple PDF generation operations before
214         // completion of an ongoing one.
215         // TODO(cimamoglu): Whenever onLayout is called, generate a new PDF with the new
216         //                  parameters. Hence, we can get the true number of pages.
217         if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
218             // We don't start a new Chromium PDF generation operation if there's an existing
219             // onLayout going on. Use the last known valid page count.
220             pageCountEstimationDone(mLastKnownMaxPages);
221         } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONWRITE) {
222             callback.onLayoutFailed(mErrorMessage);
223             resetCallbacks();
224         } else if (mPrintable.print()) {
225             mPrintingState = PRINTING_STATE_STARTED_FROM_ONLAYOUT;
226         } else {
227             callback.onLayoutFailed(mErrorMessage);
228             resetCallbacks();
229         }
230     }
231 
232     @Override
pageCountEstimationDone(final int maxPages)233     public void pageCountEstimationDone(final int maxPages) {
234         // This method might be called even after onFinish, e.g. as a result of a long page
235         // estimation operation.  We make sure that such call has no effect, since the printing
236         // dialog has already been dismissed and relevant cleanup has already been done.
237         // Also, this ensures that we do not call askUserForSettingsReply twice.
238         if (mPrintingState == PRINTING_STATE_FINISHED) return;
239         if (maxPages != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
240             mLastKnownMaxPages = maxPages;
241         }
242         if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
243             PrintDocumentInfo info = new PrintDocumentInfo.Builder(mPrintable.getTitle())
244                     .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
245                     .setPageCount(mLastKnownMaxPages)
246                     .build();
247             mOnLayoutCallback.onLayoutFinished(info, mNeedNewPdf);
248         } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONWRITE) {
249             // Chromium PDF generation is started inside onWrite, continue that.
250             if (mPrintingContext == null) {
251                 mOnWriteCallback.onWriteFailed(mErrorMessage);
252                 resetCallbacks();
253                 return;
254             }
255             mPrintingContext.askUserForSettingsReply(true);
256         }
257     }
258 
259     @Override
onWrite( final PageRange[] ranges, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final PrintDocumentAdapterWrapper.WriteResultCallbackWrapper callback)260     public void onWrite(
261             final PageRange[] ranges,
262             final ParcelFileDescriptor destination,
263             final CancellationSignal cancellationSignal,
264             final PrintDocumentAdapterWrapper.WriteResultCallbackWrapper callback) {
265         if (mPrintingContext == null) {
266             callback.onWriteFailed(mErrorMessage);
267             resetCallbacks();
268             return;
269         }
270 
271         // TODO(cimamoglu): Make use of CancellationSignal.
272         mOnWriteCallback = callback;
273 
274         mFileDescriptor = destination.getFd();
275         // Update file descriptor to PrintingContext mapping in the owner class.
276         mPrintingContext.updatePrintingContextMap(mFileDescriptor, false);
277 
278         // We need to convert ranges list into an array of individual numbers for
279         // easier JNI passing and compatibility with the native side.
280         if (ranges.length == 1 && ranges[0].equals(PageRange.ALL_PAGES)) {
281             // null corresponds to all pages in Chromium printing logic.
282             mPages = null;
283         } else {
284             mPages = normalizeRanges(ranges);
285         }
286 
287         if (mPrintingState == PRINTING_STATE_READY) {
288             // If this onWrite is without a preceding onLayout, start Chromium PDF generation here.
289             if (mPrintable.print()) {
290                 mPrintingState = PRINTING_STATE_STARTED_FROM_ONWRITE;
291             } else {
292                 callback.onWriteFailed(mErrorMessage);
293                 resetCallbacks();
294             }
295         } else if (mPrintingState == PRINTING_STATE_STARTED_FROM_ONLAYOUT) {
296             // Otherwise, continue previously started operation.
297             mPrintingContext.askUserForSettingsReply(true);
298         }
299         // We are guaranteed by the framework that we will not have two onWrite calls at once.
300         // We may get a CancellationSignal, after replying it (via WriteResultCallback) we might
301         // get another onWrite call.
302     }
303 
304     @Override
onFinish()305     public void onFinish() {
306         mLastKnownMaxPages = PrintDocumentInfo.PAGE_COUNT_UNKNOWN;
307         mPages = null;
308 
309         if (mPrintingContext != null) {
310             if (mPrintingState != PRINTING_STATE_READY) {
311                 // Note that we are never making an extraneous askUserForSettingsReply call.
312                 // If we are in the middle of a PDF generation from onLayout or onWrite, it means
313                 // the state isn't PRINTING_STATE_READY, so we enter here and make this call (no
314                 // extra). If we complete the PDF generation successfully from onLayout or onWrite,
315                 // we already make the state PRINTING_STATE_READY and call askUserForSettingsReply
316                 // inside pdfWritingDone, thus not entering here.  Also, if we get an extra
317                 // AskUserForSettings call, it's handled inside {@link
318                 // PrintingContext#pageCountEstimationDone}.
319                 mPrintingContext.askUserForSettingsReply(false);
320             }
321             mPrintingContext.updatePrintingContextMap(mFileDescriptor, true);
322             mPrintingContext = null;
323         }
324         mPrintingState = PRINTING_STATE_FINISHED;
325 
326         closeFileDescriptor(mFileDescriptor);
327         mFileDescriptor = -1;
328 
329         resetCallbacks();
330         // The printmanager contract is that onFinish() is always called as the last
331         // callback. We set busy to false here.
332         mIsBusy = false;
333     }
334 
resetCallbacks()335     private void resetCallbacks() {
336         mOnWriteCallback = null;
337         mOnLayoutCallback = null;
338     }
339 
closeFileDescriptor(int fd)340     private static void closeFileDescriptor(int fd) {
341         if (fd != -1) return;
342         ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.adoptFd(fd);
343         if (fileDescriptor != null) {
344             try {
345                 fileDescriptor.close();
346             } catch (IOException ioe) {
347                 /* ignore */
348             }
349         }
350     }
351 
convertIntegerArrayToPageRanges(int[] pagesArray)352     private static PageRange[] convertIntegerArrayToPageRanges(int[] pagesArray) {
353         PageRange[] pageRanges;
354         if (pagesArray != null) {
355             pageRanges = new PageRange[pagesArray.length];
356             for (int i = 0; i < pageRanges.length; i++) {
357                 int page = pagesArray[i];
358                 pageRanges[i] = new PageRange(page, page);
359             }
360         } else {
361             // null corresponds to all pages in Chromium printing logic.
362             pageRanges = new PageRange[] { PageRange.ALL_PAGES };
363         }
364         return pageRanges;
365     }
366 
367     /**
368      * Gets an array of page ranges and returns an array of integers with all ranges expanded.
369      */
normalizeRanges(final PageRange[] ranges)370     private static int[] normalizeRanges(final PageRange[] ranges) {
371         // Expand ranges into a list of individual numbers.
372         ArrayList<Integer> pages = new ArrayList<Integer>();
373         for (PageRange range : ranges) {
374             for (int i = range.getStart(); i <= range.getEnd(); i++) {
375                 pages.add(i);
376             }
377         }
378 
379         // Convert the list into array.
380         int[] ret = new int[pages.size()];
381         Iterator<Integer> iterator = pages.iterator();
382         for (int i = 0; i < ret.length; i++) {
383             ret[i] = iterator.next().intValue();
384         }
385         return ret;
386     }
387 }
388