16e045616abde9cf6d3c758e141c17de4e057b9a
[platform/framework/web/crosswalk.git] / src / printing / android / java / src / org / chromium / printing / PrintingControllerImpl.java
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
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      */
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      */
126     public static PrintingController getInstance() {
127         return sInstance;
128     }
129
130     @Override
131     public boolean hasPrintingFinished() {
132         return mPrintingState == PRINTING_STATE_FINISHED;
133     }
134
135     @Override
136     public int getDpi() {
137         return mDpi;
138     }
139
140     @Override
141     public int getFileDescriptor() {
142         return mFileDescriptor;
143     }
144
145     @Override
146     public int getPageHeight() {
147         return mMediaSize.getHeightMils();
148     }
149
150     @Override
151     public int getPageWidth() {
152         return mMediaSize.getWidthMils();
153     }
154
155     @Override
156     public int[] getPageNumbers() {
157         return mPages == null ? null : mPages.clone();
158     }
159
160     @Override
161     public boolean isBusy() {
162         return mIsBusy;
163     }
164
165     @Override
166     public void setPrintingContext(final PrintingContextInterface printingContext) {
167         mPrintingContext = printingContext;
168     }
169
170     @Override
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
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
194     public void onStart() {
195         mPrintingState = PRINTING_STATE_READY;
196     }
197
198     @Override
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
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
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
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
335     private void resetCallbacks() {
336         mOnWriteCallback = null;
337         mOnLayoutCallback = null;
338     }
339
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
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      */
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 }