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