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 android.os.Bundle;
8import android.os.CancellationSignal;
9import android.os.ParcelFileDescriptor;
10import android.print.PageRange;
11import android.print.PrintAttributes;
12import android.print.PrintDocumentInfo;
13
14import org.chromium.base.ThreadUtils;
15import org.chromium.printing.PrintDocumentAdapterWrapper.PdfGenerator;
16
17import java.io.IOException;
18import java.util.ArrayList;
19import 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 */
29public 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}
388