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