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