PrintManager.java revision 9dcb86a48d73f399fb1b5c020005d76d350eeac2
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.print; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.app.Activity; 22import android.app.Application.ActivityLifecycleCallbacks; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.IntentSender; 26import android.content.IntentSender.SendIntentException; 27import android.graphics.drawable.Icon; 28import android.os.Bundle; 29import android.os.CancellationSignal; 30import android.os.Handler; 31import android.os.ICancellationSignal; 32import android.os.Looper; 33import android.os.Message; 34import android.os.ParcelFileDescriptor; 35import android.os.RemoteException; 36import android.print.PrintDocumentAdapter.LayoutResultCallback; 37import android.print.PrintDocumentAdapter.WriteResultCallback; 38import android.printservice.PrintServiceInfo; 39import android.printservice.recommendation.IRecommendationsChangeListener; 40import android.printservice.recommendation.RecommendationInfo; 41import android.text.TextUtils; 42import android.util.ArrayMap; 43import android.util.Log; 44 45import com.android.internal.os.SomeArgs; 46 47import com.android.internal.util.Preconditions; 48import libcore.io.IoUtils; 49 50import java.lang.ref.WeakReference; 51import java.util.ArrayList; 52import java.util.Arrays; 53import java.util.Collections; 54import java.util.List; 55import java.util.Map; 56 57/** 58 * System level service for accessing the printing capabilities of the platform. 59 * <p> 60 * To obtain a handle to the print manager do the following: 61 * </p> 62 * 63 * <pre> 64 * PrintManager printManager = 65 * (PrintManager) context.getSystemService(Context.PRINT_SERVICE); 66 * </pre> 67 * 68 * <h3>Print mechanics</h3> 69 * <p> 70 * The key idea behind printing on the platform is that the content to be printed 71 * should be laid out for the currently selected print options resulting in an 72 * optimized output and higher user satisfaction. To achieve this goal the platform 73 * declares a contract that the printing application has to follow which is defined 74 * by the {@link PrintDocumentAdapter} class. At a higher level the contract is that 75 * when the user selects some options from the print UI that may affect the way 76 * content is laid out, for example page size, the application receives a callback 77 * allowing it to layout the content to better fit these new constraints. After a 78 * layout pass the system may ask the application to render one or more pages one 79 * or more times. For example, an application may produce a single column list for 80 * smaller page sizes and a multi-column table for larger page sizes. 81 * </p> 82 * <h3>Print jobs</h3> 83 * <p> 84 * Print jobs are started by calling the {@link #print(String, PrintDocumentAdapter, 85 * PrintAttributes)} from an activity which results in bringing up the system print 86 * UI. Once the print UI is up, when the user changes a selected print option that 87 * affects the way content is laid out the system starts to interact with the 88 * application following the mechanics described the section above. 89 * </p> 90 * <p> 91 * Print jobs can be in {@link PrintJobInfo#STATE_CREATED created}, {@link 92 * PrintJobInfo#STATE_QUEUED queued}, {@link PrintJobInfo#STATE_STARTED started}, 93 * {@link PrintJobInfo#STATE_BLOCKED blocked}, {@link PrintJobInfo#STATE_COMPLETED 94 * completed}, {@link PrintJobInfo#STATE_FAILED failed}, and {@link 95 * PrintJobInfo#STATE_CANCELED canceled} state. Print jobs are stored in dedicated 96 * system spooler until they are handled which is they are cancelled or completed. 97 * Active print jobs, ones that are not cancelled or completed, are considered failed 98 * if the device reboots as the new boot may be after a very long time. The user may 99 * choose to restart such print jobs. Once a print job is queued all relevant content 100 * is stored in the system spooler and its lifecycle becomes detached from this of 101 * the application that created it. 102 * </p> 103 * <p> 104 * An applications can query the print spooler for current print jobs it created 105 * but not print jobs created by other applications. 106 * </p> 107 * 108 * @see PrintJob 109 * @see PrintJobInfo 110 */ 111public final class PrintManager { 112 113 private static final String LOG_TAG = "PrintManager"; 114 115 private static final boolean DEBUG = false; 116 117 private static final int MSG_NOTIFY_PRINT_JOB_STATE_CHANGED = 1; 118 private static final int MSG_NOTIFY_PRINT_SERVICES_CHANGED = 2; 119 private static final int MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED = 3; 120 121 /** 122 * Package name of print spooler. 123 * 124 * @hide 125 */ 126 public static final String PRINT_SPOOLER_PACKAGE_NAME = "com.android.printspooler"; 127 128 /** 129 * Select enabled services. 130 * </p> 131 * @see #getPrintServices 132 * @hide 133 */ 134 public static final int ENABLED_SERVICES = 1 << 0; 135 136 /** 137 * Select disabled services. 138 * </p> 139 * @see #getPrintServices 140 * @hide 141 */ 142 public static final int DISABLED_SERVICES = 1 << 1; 143 144 /** 145 * Select all services. 146 * </p> 147 * @see #getPrintServices 148 * @hide 149 */ 150 public static final int ALL_SERVICES = ENABLED_SERVICES | DISABLED_SERVICES; 151 152 /** 153 * The action for launching the print dialog activity. 154 * 155 * @hide 156 */ 157 public static final String ACTION_PRINT_DIALOG = "android.print.PRINT_DIALOG"; 158 159 /** 160 * Extra with the intent for starting the print dialog. 161 * <p> 162 * <strong>Type:</strong> {@link android.content.IntentSender} 163 * </p> 164 * 165 * @hide 166 */ 167 public static final String EXTRA_PRINT_DIALOG_INTENT = 168 "android.print.intent.extra.EXTRA_PRINT_DIALOG_INTENT"; 169 170 /** 171 * Extra with a print job. 172 * <p> 173 * <strong>Type:</strong> {@link android.print.PrintJobInfo} 174 * </p> 175 * 176 * @hide 177 */ 178 public static final String EXTRA_PRINT_JOB = 179 "android.print.intent.extra.EXTRA_PRINT_JOB"; 180 181 /** 182 * Extra with the print document adapter to be printed. 183 * <p> 184 * <strong>Type:</strong> {@link android.print.IPrintDocumentAdapter} 185 * </p> 186 * 187 * @hide 188 */ 189 public static final String EXTRA_PRINT_DOCUMENT_ADAPTER = 190 "android.print.intent.extra.EXTRA_PRINT_DOCUMENT_ADAPTER"; 191 192 /** @hide */ 193 public static final int APP_ID_ANY = -2; 194 195 private final Context mContext; 196 197 private final IPrintManager mService; 198 199 private final int mUserId; 200 201 private final int mAppId; 202 203 private final Handler mHandler; 204 205 private Map<PrintJobStateChangeListener, PrintJobStateChangeListenerWrapper> 206 mPrintJobStateChangeListeners; 207 private Map<PrintServicesChangeListener, PrintServicesChangeListenerWrapper> 208 mPrintServicesChangeListeners; 209 private Map<PrintServiceRecommendationsChangeListener, 210 PrintServiceRecommendationsChangeListenerWrapper> 211 mPrintServiceRecommendationsChangeListeners; 212 213 /** @hide */ 214 public interface PrintJobStateChangeListener { 215 216 /** 217 * Callback notifying that a print job state changed. 218 * 219 * @param printJobId The print job id. 220 */ 221 public void onPrintJobStateChanged(PrintJobId printJobId); 222 } 223 224 /** @hide */ 225 public interface PrintServicesChangeListener { 226 227 /** 228 * Callback notifying that the print services changed. 229 */ 230 public void onPrintServicesChanged(); 231 } 232 233 /** @hide */ 234 public interface PrintServiceRecommendationsChangeListener { 235 236 /** 237 * Callback notifying that the print service recommendations changed. 238 */ 239 void onPrintServiceRecommendationsChanged(); 240 } 241 242 /** 243 * Creates a new instance. 244 * 245 * @param context The current context in which to operate. 246 * @param service The backing system service. 247 * @param userId The user id in which to operate. 248 * @param appId The application id in which to operate. 249 * @hide 250 */ 251 public PrintManager(Context context, IPrintManager service, int userId, int appId) { 252 mContext = context; 253 mService = service; 254 mUserId = userId; 255 mAppId = appId; 256 mHandler = new Handler(context.getMainLooper(), null, false) { 257 @Override 258 public void handleMessage(Message message) { 259 switch (message.what) { 260 case MSG_NOTIFY_PRINT_JOB_STATE_CHANGED: { 261 SomeArgs args = (SomeArgs) message.obj; 262 PrintJobStateChangeListenerWrapper wrapper = 263 (PrintJobStateChangeListenerWrapper) args.arg1; 264 PrintJobStateChangeListener listener = wrapper.getListener(); 265 if (listener != null) { 266 PrintJobId printJobId = (PrintJobId) args.arg2; 267 listener.onPrintJobStateChanged(printJobId); 268 } 269 args.recycle(); 270 } break; 271 case MSG_NOTIFY_PRINT_SERVICES_CHANGED: { 272 PrintServicesChangeListenerWrapper wrapper = 273 (PrintServicesChangeListenerWrapper) message.obj; 274 PrintServicesChangeListener listener = wrapper.getListener(); 275 if (listener != null) { 276 listener.onPrintServicesChanged(); 277 } 278 } break; 279 case MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED: { 280 PrintServiceRecommendationsChangeListenerWrapper wrapper = 281 (PrintServiceRecommendationsChangeListenerWrapper) message.obj; 282 PrintServiceRecommendationsChangeListener listener = wrapper.getListener(); 283 if (listener != null) { 284 listener.onPrintServiceRecommendationsChanged(); 285 } 286 } break; 287 } 288 } 289 }; 290 } 291 292 /** 293 * Creates an instance that can access all print jobs. 294 * 295 * @param userId The user id for which to get all print jobs. 296 * @return An instance if the caller has the permission to access all print 297 * jobs, null otherwise. 298 * @hide 299 */ 300 public PrintManager getGlobalPrintManagerForUser(int userId) { 301 if (mService == null) { 302 Log.w(LOG_TAG, "Feature android.software.print not available"); 303 return null; 304 } 305 return new PrintManager(mContext, mService, userId, APP_ID_ANY); 306 } 307 308 PrintJobInfo getPrintJobInfo(PrintJobId printJobId) { 309 try { 310 return mService.getPrintJobInfo(printJobId, mAppId, mUserId); 311 } catch (RemoteException re) { 312 throw re.rethrowFromSystemServer(); 313 } 314 } 315 316 /** 317 * Adds a listener for observing the state of print jobs. 318 * 319 * @param listener The listener to add. 320 * @hide 321 */ 322 public void addPrintJobStateChangeListener(PrintJobStateChangeListener listener) { 323 if (mService == null) { 324 Log.w(LOG_TAG, "Feature android.software.print not available"); 325 return; 326 } 327 if (mPrintJobStateChangeListeners == null) { 328 mPrintJobStateChangeListeners = new ArrayMap<PrintJobStateChangeListener, 329 PrintJobStateChangeListenerWrapper>(); 330 } 331 PrintJobStateChangeListenerWrapper wrappedListener = 332 new PrintJobStateChangeListenerWrapper(listener, mHandler); 333 try { 334 mService.addPrintJobStateChangeListener(wrappedListener, mAppId, mUserId); 335 mPrintJobStateChangeListeners.put(listener, wrappedListener); 336 } catch (RemoteException re) { 337 throw re.rethrowFromSystemServer(); 338 } 339 } 340 341 /** 342 * Removes a listener for observing the state of print jobs. 343 * 344 * @param listener The listener to remove. 345 * @hide 346 */ 347 public void removePrintJobStateChangeListener(PrintJobStateChangeListener listener) { 348 if (mService == null) { 349 Log.w(LOG_TAG, "Feature android.software.print not available"); 350 return; 351 } 352 if (mPrintJobStateChangeListeners == null) { 353 return; 354 } 355 PrintJobStateChangeListenerWrapper wrappedListener = 356 mPrintJobStateChangeListeners.remove(listener); 357 if (wrappedListener == null) { 358 return; 359 } 360 if (mPrintJobStateChangeListeners.isEmpty()) { 361 mPrintJobStateChangeListeners = null; 362 } 363 wrappedListener.destroy(); 364 try { 365 mService.removePrintJobStateChangeListener(wrappedListener, mUserId); 366 } catch (RemoteException re) { 367 throw re.rethrowFromSystemServer(); 368 } 369 } 370 371 /** 372 * Gets a print job given its id. 373 * 374 * @param printJobId The id of the print job. 375 * @return The print job list. 376 * @see PrintJob 377 * @hide 378 */ 379 public PrintJob getPrintJob(PrintJobId printJobId) { 380 if (mService == null) { 381 Log.w(LOG_TAG, "Feature android.software.print not available"); 382 return null; 383 } 384 try { 385 PrintJobInfo printJob = mService.getPrintJobInfo(printJobId, mAppId, mUserId); 386 if (printJob != null) { 387 return new PrintJob(printJob, this); 388 } 389 } catch (RemoteException re) { 390 throw re.rethrowFromSystemServer(); 391 } 392 return null; 393 } 394 395 /** 396 * Get the custom icon for a printer. If the icon is not cached, the icon is 397 * requested asynchronously. Once it is available the printer is updated. 398 * 399 * @param printerId the id of the printer the icon should be loaded for 400 * @return the custom icon to be used for the printer or null if the icon is 401 * not yet available 402 * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon() 403 * @hide 404 */ 405 public Icon getCustomPrinterIcon(PrinterId printerId) { 406 if (mService == null) { 407 Log.w(LOG_TAG, "Feature android.software.print not available"); 408 return null; 409 } 410 try { 411 return mService.getCustomPrinterIcon(printerId, mUserId); 412 } catch (RemoteException re) { 413 throw re.rethrowFromSystemServer(); 414 } 415 } 416 417 /** 418 * Gets the print jobs for this application. 419 * 420 * @return The print job list. 421 * @see PrintJob 422 */ 423 public @NonNull List<PrintJob> getPrintJobs() { 424 if (mService == null) { 425 Log.w(LOG_TAG, "Feature android.software.print not available"); 426 return Collections.emptyList(); 427 } 428 try { 429 List<PrintJobInfo> printJobInfos = mService.getPrintJobInfos(mAppId, mUserId); 430 if (printJobInfos == null) { 431 return Collections.emptyList(); 432 } 433 final int printJobCount = printJobInfos.size(); 434 List<PrintJob> printJobs = new ArrayList<PrintJob>(printJobCount); 435 for (int i = 0; i < printJobCount; i++) { 436 printJobs.add(new PrintJob(printJobInfos.get(i), this)); 437 } 438 return printJobs; 439 } catch (RemoteException re) { 440 throw re.rethrowFromSystemServer(); 441 } 442 } 443 444 void cancelPrintJob(PrintJobId printJobId) { 445 if (mService == null) { 446 Log.w(LOG_TAG, "Feature android.software.print not available"); 447 return; 448 } 449 try { 450 mService.cancelPrintJob(printJobId, mAppId, mUserId); 451 } catch (RemoteException re) { 452 throw re.rethrowFromSystemServer(); 453 } 454 } 455 456 void restartPrintJob(PrintJobId printJobId) { 457 if (mService == null) { 458 Log.w(LOG_TAG, "Feature android.software.print not available"); 459 return; 460 } 461 try { 462 mService.restartPrintJob(printJobId, mAppId, mUserId); 463 } catch (RemoteException re) { 464 throw re.rethrowFromSystemServer(); 465 } 466 } 467 468 /** 469 * Creates a print job for printing a {@link PrintDocumentAdapter} with 470 * default print attributes. 471 * <p> 472 * Calling this method brings the print UI allowing the user to customize 473 * the print job and returns a {@link PrintJob} object without waiting for the 474 * user to customize or confirm the print job. The returned print job instance 475 * is in a {@link PrintJobInfo#STATE_CREATED created} state. 476 * <p> 477 * This method can be called only from an {@link Activity}. The rationale is that 478 * printing from a service will create an inconsistent user experience as the print 479 * UI would appear without any context. 480 * </p> 481 * <p> 482 * Also the passed in {@link PrintDocumentAdapter} will be considered invalid if 483 * your activity is finished. The rationale is that once the activity that 484 * initiated printing is finished, the provided adapter may be in an inconsistent 485 * state as it may depend on the UI presented by the activity. 486 * </p> 487 * <p> 488 * The default print attributes are a hint to the system how the data is to 489 * be printed. For example, a photo editor may look at the photo aspect ratio 490 * to determine the default orientation and provide a hint whether the printing 491 * should be in portrait or landscape. The system will do a best effort to 492 * selected the hinted options in the print dialog, given the current printer 493 * supports them. 494 * </p> 495 * <p> 496 * <strong>Note:</strong> Calling this method will bring the print dialog and 497 * the system will connect to the provided {@link PrintDocumentAdapter}. If a 498 * configuration change occurs that you application does not handle, for example 499 * a rotation change, the system will drop the connection to the adapter as the 500 * activity has to be recreated and the old adapter may be invalid in this context, 501 * hence a new adapter instance is required. As a consequence, if your activity 502 * does not handle configuration changes (default behavior), you have to save the 503 * state that you were printing and call this method again when your activity 504 * is recreated. 505 * </p> 506 * 507 * @param printJobName A name for the new print job which is shown to the user. 508 * @param documentAdapter An adapter that emits the document to print. 509 * @param attributes The default print job attributes or <code>null</code>. 510 * @return The created print job on success or null on failure. 511 * @throws IllegalStateException If not called from an {@link Activity}. 512 * @throws IllegalArgumentException If the print job name is empty or the 513 * document adapter is null. 514 * 515 * @see PrintJob 516 */ 517 public @NonNull PrintJob print(@NonNull String printJobName, 518 @NonNull PrintDocumentAdapter documentAdapter, 519 @Nullable PrintAttributes attributes) { 520 if (mService == null) { 521 Log.w(LOG_TAG, "Feature android.software.print not available"); 522 return null; 523 } 524 if (!(mContext instanceof Activity)) { 525 throw new IllegalStateException("Can print only from an activity"); 526 } 527 if (TextUtils.isEmpty(printJobName)) { 528 throw new IllegalArgumentException("printJobName cannot be empty"); 529 } 530 if (documentAdapter == null) { 531 throw new IllegalArgumentException("documentAdapter cannot be null"); 532 } 533 PrintDocumentAdapterDelegate delegate = new PrintDocumentAdapterDelegate( 534 (Activity) mContext, documentAdapter); 535 try { 536 Bundle result = mService.print(printJobName, delegate, 537 attributes, mContext.getPackageName(), mAppId, mUserId); 538 if (result != null) { 539 PrintJobInfo printJob = result.getParcelable(EXTRA_PRINT_JOB); 540 IntentSender intent = result.getParcelable(EXTRA_PRINT_DIALOG_INTENT); 541 if (printJob == null || intent == null) { 542 return null; 543 } 544 try { 545 mContext.startIntentSender(intent, null, 0, 0, 0); 546 return new PrintJob(printJob, this); 547 } catch (SendIntentException sie) { 548 Log.e(LOG_TAG, "Couldn't start print job config activity.", sie); 549 } 550 } 551 } catch (RemoteException re) { 552 throw re.rethrowFromSystemServer(); 553 } 554 return null; 555 } 556 557 /** 558 * Listen for changes to the installed and enabled print services. 559 * 560 * @param listener the listener to add 561 * 562 * @see android.print.PrintManager#getPrintServices 563 */ 564 void addPrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) { 565 Preconditions.checkNotNull(listener); 566 567 if (mService == null) { 568 Log.w(LOG_TAG, "Feature android.software.print not available"); 569 return; 570 } 571 if (mPrintServicesChangeListeners == null) { 572 mPrintServicesChangeListeners = new ArrayMap<>(); 573 } 574 PrintServicesChangeListenerWrapper wrappedListener = 575 new PrintServicesChangeListenerWrapper(listener, mHandler); 576 try { 577 mService.addPrintServicesChangeListener(wrappedListener, mUserId); 578 mPrintServicesChangeListeners.put(listener, wrappedListener); 579 } catch (RemoteException re) { 580 throw re.rethrowFromSystemServer(); 581 } 582 } 583 584 /** 585 * Stop listening for changes to the installed and enabled print services. 586 * 587 * @param listener the listener to remove 588 * 589 * @see android.print.PrintManager#getPrintServices 590 */ 591 void removePrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) { 592 Preconditions.checkNotNull(listener); 593 594 if (mService == null) { 595 Log.w(LOG_TAG, "Feature android.software.print not available"); 596 return; 597 } 598 if (mPrintServicesChangeListeners == null) { 599 return; 600 } 601 PrintServicesChangeListenerWrapper wrappedListener = 602 mPrintServicesChangeListeners.remove(listener); 603 if (wrappedListener == null) { 604 return; 605 } 606 if (mPrintServicesChangeListeners.isEmpty()) { 607 mPrintServicesChangeListeners = null; 608 } 609 wrappedListener.destroy(); 610 try { 611 mService.removePrintServicesChangeListener(wrappedListener, mUserId); 612 } catch (RemoteException re) { 613 Log.e(LOG_TAG, "Error removing print services change listener", re); 614 } 615 } 616 617 /** 618 * Gets the list of print services, but does not register for updates. The user has to register 619 * for updates by itself, or use {@link PrintServicesLoader}. 620 * 621 * @param selectionFlags flags selecting which services to get. Either 622 * {@link #ENABLED_SERVICES},{@link #DISABLED_SERVICES}, or both. 623 * 624 * @return The print service list or an empty list. 625 * 626 * @see #addPrintServicesChangeListener(PrintServicesChangeListener) 627 * @see #removePrintServicesChangeListener(PrintServicesChangeListener) 628 * 629 * @hide 630 */ 631 public @NonNull List<PrintServiceInfo> getPrintServices(int selectionFlags) { 632 Preconditions.checkFlagsArgument(selectionFlags, ALL_SERVICES); 633 634 try { 635 List<PrintServiceInfo> services = mService.getPrintServices(selectionFlags, mUserId); 636 if (services != null) { 637 return services; 638 } 639 } catch (RemoteException re) { 640 throw re.rethrowFromSystemServer(); 641 } 642 return Collections.emptyList(); 643 } 644 645 /** 646 * Listen for changes to the print service recommendations. 647 * 648 * @param listener the listener to add 649 * 650 * @see android.print.PrintManager#getPrintServiceRecommendations 651 */ 652 void addPrintServiceRecommendationsChangeListener( 653 @NonNull PrintServiceRecommendationsChangeListener listener) { 654 Preconditions.checkNotNull(listener); 655 656 if (mService == null) { 657 Log.w(LOG_TAG, "Feature android.software.print not available"); 658 return; 659 } 660 if (mPrintServiceRecommendationsChangeListeners == null) { 661 mPrintServiceRecommendationsChangeListeners = new ArrayMap<>(); 662 } 663 PrintServiceRecommendationsChangeListenerWrapper wrappedListener = 664 new PrintServiceRecommendationsChangeListenerWrapper(listener, mHandler); 665 try { 666 mService.addPrintServiceRecommendationsChangeListener(wrappedListener, mUserId); 667 mPrintServiceRecommendationsChangeListeners.put(listener, wrappedListener); 668 } catch (RemoteException re) { 669 throw re.rethrowFromSystemServer(); 670 } 671 } 672 673 /** 674 * Stop listening for changes to the print service recommendations. 675 * 676 * @param listener the listener to remove 677 * 678 * @see android.print.PrintManager#getPrintServiceRecommendations 679 */ 680 void removePrintServiceRecommendationsChangeListener( 681 @NonNull PrintServiceRecommendationsChangeListener listener) { 682 Preconditions.checkNotNull(listener); 683 684 if (mService == null) { 685 Log.w(LOG_TAG, "Feature android.software.print not available"); 686 return; 687 } 688 if (mPrintServiceRecommendationsChangeListeners == null) { 689 return; 690 } 691 PrintServiceRecommendationsChangeListenerWrapper wrappedListener = 692 mPrintServiceRecommendationsChangeListeners.remove(listener); 693 if (wrappedListener == null) { 694 return; 695 } 696 if (mPrintServiceRecommendationsChangeListeners.isEmpty()) { 697 mPrintServiceRecommendationsChangeListeners = null; 698 } 699 wrappedListener.destroy(); 700 try { 701 mService.removePrintServiceRecommendationsChangeListener(wrappedListener, mUserId); 702 } catch (RemoteException re) { 703 throw re.rethrowFromSystemServer(); 704 } 705 } 706 707 /** 708 * Gets the list of print service recommendations, but does not register for updates. The user 709 * has to register for updates by itself, or use {@link PrintServiceRecommendationsLoader}. 710 * 711 * @return The print service recommendations list or an empty list. 712 * 713 * @see #addPrintServiceRecommendationsChangeListener 714 * @see #removePrintServiceRecommendationsChangeListener 715 * 716 * @hide 717 */ 718 public @NonNull List<RecommendationInfo> getPrintServiceRecommendations() { 719 try { 720 List<RecommendationInfo> recommendations = 721 mService.getPrintServiceRecommendations(mUserId); 722 if (recommendations != null) { 723 return recommendations; 724 } 725 } catch (RemoteException re) { 726 throw re.rethrowFromSystemServer(); 727 } 728 return Collections.emptyList(); 729 } 730 731 /** 732 * @hide 733 */ 734 public PrinterDiscoverySession createPrinterDiscoverySession() { 735 if (mService == null) { 736 Log.w(LOG_TAG, "Feature android.software.print not available"); 737 return null; 738 } 739 return new PrinterDiscoverySession(mService, mContext, mUserId); 740 } 741 742 /** 743 * Enable or disable a print service. 744 * 745 * @param service The service to enabled or disable 746 * @param isEnabled whether the service should be enabled or disabled 747 * 748 * @hide 749 */ 750 public void setPrintServiceEnabled(@NonNull ComponentName service, boolean isEnabled) { 751 if (mService == null) { 752 Log.w(LOG_TAG, "Feature android.software.print not available"); 753 return; 754 } 755 try { 756 mService.setPrintServiceEnabled(service, isEnabled, mUserId); 757 } catch (RemoteException re) { 758 Log.e(LOG_TAG, "Error enabling or disabling " + service, re); 759 } 760 } 761 762 /** 763 * @hide 764 */ 765 public static final class PrintDocumentAdapterDelegate extends IPrintDocumentAdapter.Stub 766 implements ActivityLifecycleCallbacks { 767 private final Object mLock = new Object(); 768 769 private Activity mActivity; // Strong reference OK - cleared in destroy 770 771 private PrintDocumentAdapter mDocumentAdapter; // Strong reference OK - cleared in destroy 772 773 private Handler mHandler; // Strong reference OK - cleared in destroy 774 775 private IPrintDocumentAdapterObserver mObserver; // Strong reference OK - cleared in destroy 776 777 private DestroyableCallback mPendingCallback; 778 779 public PrintDocumentAdapterDelegate(Activity activity, 780 PrintDocumentAdapter documentAdapter) { 781 mActivity = activity; 782 mDocumentAdapter = documentAdapter; 783 mHandler = new MyHandler(mActivity.getMainLooper()); 784 mActivity.getApplication().registerActivityLifecycleCallbacks(this); 785 } 786 787 @Override 788 public void setObserver(IPrintDocumentAdapterObserver observer) { 789 final boolean destroyed; 790 synchronized (mLock) { 791 mObserver = observer; 792 destroyed = isDestroyedLocked(); 793 } 794 795 if (destroyed && observer != null) { 796 try { 797 observer.onDestroy(); 798 } catch (RemoteException re) { 799 Log.e(LOG_TAG, "Error announcing destroyed state", re); 800 } 801 } 802 } 803 804 @Override 805 public void start() { 806 synchronized (mLock) { 807 // If destroyed the handler is null. 808 if (!isDestroyedLocked()) { 809 mHandler.obtainMessage(MyHandler.MSG_ON_START, 810 mDocumentAdapter).sendToTarget(); 811 } 812 } 813 } 814 815 @Override 816 public void layout(PrintAttributes oldAttributes, PrintAttributes newAttributes, 817 ILayoutResultCallback callback, Bundle metadata, int sequence) { 818 819 ICancellationSignal cancellationTransport = CancellationSignal.createTransport(); 820 try { 821 callback.onLayoutStarted(cancellationTransport, sequence); 822 } catch (RemoteException re) { 823 // The spooler is dead - can't recover. 824 Log.e(LOG_TAG, "Error notifying for layout start", re); 825 return; 826 } 827 828 synchronized (mLock) { 829 // If destroyed the handler is null. 830 if (isDestroyedLocked()) { 831 return; 832 } 833 834 CancellationSignal cancellationSignal = CancellationSignal.fromTransport( 835 cancellationTransport); 836 837 SomeArgs args = SomeArgs.obtain(); 838 args.arg1 = mDocumentAdapter; 839 args.arg2 = oldAttributes; 840 args.arg3 = newAttributes; 841 args.arg4 = cancellationSignal; 842 args.arg5 = new MyLayoutResultCallback(callback, sequence); 843 args.arg6 = metadata; 844 845 mHandler.obtainMessage(MyHandler.MSG_ON_LAYOUT, args).sendToTarget(); 846 } 847 } 848 849 @Override 850 public void write(PageRange[] pages, ParcelFileDescriptor fd, 851 IWriteResultCallback callback, int sequence) { 852 853 ICancellationSignal cancellationTransport = CancellationSignal.createTransport(); 854 try { 855 callback.onWriteStarted(cancellationTransport, sequence); 856 } catch (RemoteException re) { 857 // The spooler is dead - can't recover. 858 Log.e(LOG_TAG, "Error notifying for write start", re); 859 return; 860 } 861 862 synchronized (mLock) { 863 // If destroyed the handler is null. 864 if (isDestroyedLocked()) { 865 return; 866 } 867 868 CancellationSignal cancellationSignal = CancellationSignal.fromTransport( 869 cancellationTransport); 870 871 SomeArgs args = SomeArgs.obtain(); 872 args.arg1 = mDocumentAdapter; 873 args.arg2 = pages; 874 args.arg3 = fd; 875 args.arg4 = cancellationSignal; 876 args.arg5 = new MyWriteResultCallback(callback, fd, sequence); 877 878 mHandler.obtainMessage(MyHandler.MSG_ON_WRITE, args).sendToTarget(); 879 } 880 } 881 882 @Override 883 public void finish() { 884 synchronized (mLock) { 885 // If destroyed the handler is null. 886 if (!isDestroyedLocked()) { 887 mHandler.obtainMessage(MyHandler.MSG_ON_FINISH, 888 mDocumentAdapter).sendToTarget(); 889 } 890 } 891 } 892 893 @Override 894 public void kill(String reason) { 895 synchronized (mLock) { 896 // If destroyed the handler is null. 897 if (!isDestroyedLocked()) { 898 mHandler.obtainMessage(MyHandler.MSG_ON_KILL, 899 reason).sendToTarget(); 900 } 901 } 902 } 903 904 @Override 905 public void onActivityPaused(Activity activity) { 906 /* do nothing */ 907 } 908 909 @Override 910 public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 911 /* do nothing */ 912 } 913 914 @Override 915 public void onActivityStarted(Activity activity) { 916 /* do nothing */ 917 } 918 919 @Override 920 public void onActivityResumed(Activity activity) { 921 /* do nothing */ 922 } 923 924 @Override 925 public void onActivityStopped(Activity activity) { 926 /* do nothing */ 927 } 928 929 @Override 930 public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 931 /* do nothing */ 932 } 933 934 @Override 935 public void onActivityDestroyed(Activity activity) { 936 // We really care only if the activity is being destroyed to 937 // notify the the print spooler so it can close the print dialog. 938 // Note the the spooler has a death recipient that observes if 939 // this process gets killed so we cover the case of onDestroy not 940 // being called due to this process being killed to reclaim memory. 941 IPrintDocumentAdapterObserver observer = null; 942 synchronized (mLock) { 943 if (activity == mActivity) { 944 observer = mObserver; 945 destroyLocked(); 946 } 947 } 948 if (observer != null) { 949 try { 950 observer.onDestroy(); 951 } catch (RemoteException re) { 952 Log.e(LOG_TAG, "Error announcing destroyed state", re); 953 } 954 } 955 } 956 957 private boolean isDestroyedLocked() { 958 return (mActivity == null); 959 } 960 961 private void destroyLocked() { 962 mActivity.getApplication().unregisterActivityLifecycleCallbacks( 963 PrintDocumentAdapterDelegate.this); 964 mActivity = null; 965 966 mDocumentAdapter = null; 967 968 // This method is only called from the main thread, so 969 // clearing the messages guarantees that any time a 970 // message is handled we are not in a destroyed state. 971 mHandler.removeMessages(MyHandler.MSG_ON_START); 972 mHandler.removeMessages(MyHandler.MSG_ON_LAYOUT); 973 mHandler.removeMessages(MyHandler.MSG_ON_WRITE); 974 mHandler.removeMessages(MyHandler.MSG_ON_FINISH); 975 mHandler = null; 976 977 mObserver = null; 978 979 if (mPendingCallback != null) { 980 mPendingCallback.destroy(); 981 mPendingCallback = null; 982 } 983 } 984 985 private final class MyHandler extends Handler { 986 public static final int MSG_ON_START = 1; 987 public static final int MSG_ON_LAYOUT = 2; 988 public static final int MSG_ON_WRITE = 3; 989 public static final int MSG_ON_FINISH = 4; 990 public static final int MSG_ON_KILL = 5; 991 992 public MyHandler(Looper looper) { 993 super(looper, null, true); 994 } 995 996 @Override 997 public void handleMessage(Message message) { 998 switch (message.what) { 999 case MSG_ON_START: { 1000 if (DEBUG) { 1001 Log.i(LOG_TAG, "onStart()"); 1002 } 1003 1004 ((PrintDocumentAdapter) message.obj).onStart(); 1005 } break; 1006 1007 case MSG_ON_LAYOUT: { 1008 SomeArgs args = (SomeArgs) message.obj; 1009 PrintDocumentAdapter adapter = (PrintDocumentAdapter) args.arg1; 1010 PrintAttributes oldAttributes = (PrintAttributes) args.arg2; 1011 PrintAttributes newAttributes = (PrintAttributes) args.arg3; 1012 CancellationSignal cancellation = (CancellationSignal) args.arg4; 1013 LayoutResultCallback callback = (LayoutResultCallback) args.arg5; 1014 Bundle metadata = (Bundle) args.arg6; 1015 args.recycle(); 1016 1017 if (DEBUG) { 1018 StringBuilder builder = new StringBuilder(); 1019 builder.append("PrintDocumentAdapter#onLayout() {\n"); 1020 builder.append("\n oldAttributes:").append(oldAttributes); 1021 builder.append("\n newAttributes:").append(newAttributes); 1022 builder.append("\n preview:").append(metadata.getBoolean( 1023 PrintDocumentAdapter.EXTRA_PRINT_PREVIEW)); 1024 builder.append("\n}"); 1025 Log.i(LOG_TAG, builder.toString()); 1026 } 1027 1028 adapter.onLayout(oldAttributes, newAttributes, cancellation, 1029 callback, metadata); 1030 } break; 1031 1032 case MSG_ON_WRITE: { 1033 SomeArgs args = (SomeArgs) message.obj; 1034 PrintDocumentAdapter adapter = (PrintDocumentAdapter) args.arg1; 1035 PageRange[] pages = (PageRange[]) args.arg2; 1036 ParcelFileDescriptor fd = (ParcelFileDescriptor) args.arg3; 1037 CancellationSignal cancellation = (CancellationSignal) args.arg4; 1038 WriteResultCallback callback = (WriteResultCallback) args.arg5; 1039 args.recycle(); 1040 1041 if (DEBUG) { 1042 StringBuilder builder = new StringBuilder(); 1043 builder.append("PrintDocumentAdapter#onWrite() {\n"); 1044 builder.append("\n pages:").append(Arrays.toString(pages)); 1045 builder.append("\n}"); 1046 Log.i(LOG_TAG, builder.toString()); 1047 } 1048 1049 adapter.onWrite(pages, fd, cancellation, callback); 1050 } break; 1051 1052 case MSG_ON_FINISH: { 1053 if (DEBUG) { 1054 Log.i(LOG_TAG, "onFinish()"); 1055 } 1056 1057 ((PrintDocumentAdapter) message.obj).onFinish(); 1058 1059 // Done printing, so destroy this instance as it 1060 // should not be used anymore. 1061 synchronized (mLock) { 1062 destroyLocked(); 1063 } 1064 } break; 1065 1066 case MSG_ON_KILL: { 1067 if (DEBUG) { 1068 Log.i(LOG_TAG, "onKill()"); 1069 } 1070 1071 String reason = (String) message.obj; 1072 throw new RuntimeException(reason); 1073 } 1074 1075 default: { 1076 throw new IllegalArgumentException("Unknown message: " 1077 + message.what); 1078 } 1079 } 1080 } 1081 } 1082 1083 private interface DestroyableCallback { 1084 public void destroy(); 1085 } 1086 1087 private final class MyLayoutResultCallback extends LayoutResultCallback 1088 implements DestroyableCallback { 1089 private ILayoutResultCallback mCallback; 1090 private final int mSequence; 1091 1092 public MyLayoutResultCallback(ILayoutResultCallback callback, 1093 int sequence) { 1094 mCallback = callback; 1095 mSequence = sequence; 1096 } 1097 1098 @Override 1099 public void onLayoutFinished(PrintDocumentInfo info, boolean changed) { 1100 final ILayoutResultCallback callback; 1101 synchronized (mLock) { 1102 callback = mCallback; 1103 } 1104 1105 // If the callback is null we are destroyed. 1106 if (callback == null) { 1107 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1108 + "finish the printing activity before print completion " 1109 + "or did you invoke a callback after finish?"); 1110 return; 1111 } 1112 1113 try { 1114 if (info == null) { 1115 throw new NullPointerException("document info cannot be null"); 1116 } 1117 1118 try { 1119 callback.onLayoutFinished(info, changed, mSequence); 1120 } catch (RemoteException re) { 1121 Log.e(LOG_TAG, "Error calling onLayoutFinished", re); 1122 } 1123 } finally { 1124 destroy(); 1125 } 1126 } 1127 1128 @Override 1129 public void onLayoutFailed(CharSequence error) { 1130 final ILayoutResultCallback callback; 1131 synchronized (mLock) { 1132 callback = mCallback; 1133 } 1134 1135 // If the callback is null we are destroyed. 1136 if (callback == null) { 1137 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1138 + "finish the printing activity before print completion " 1139 + "or did you invoke a callback after finish?"); 1140 return; 1141 } 1142 1143 try { 1144 callback.onLayoutFailed(error, mSequence); 1145 } catch (RemoteException re) { 1146 Log.e(LOG_TAG, "Error calling onLayoutFailed", re); 1147 } finally { 1148 destroy(); 1149 } 1150 } 1151 1152 @Override 1153 public void onLayoutCancelled() { 1154 final ILayoutResultCallback callback; 1155 synchronized (mLock) { 1156 callback = mCallback; 1157 } 1158 1159 // If the callback is null we are destroyed. 1160 if (callback == null) { 1161 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1162 + "finish the printing activity before print completion " 1163 + "or did you invoke a callback after finish?"); 1164 return; 1165 } 1166 1167 try { 1168 callback.onLayoutCanceled(mSequence); 1169 } catch (RemoteException re) { 1170 Log.e(LOG_TAG, "Error calling onLayoutFailed", re); 1171 } finally { 1172 destroy(); 1173 } 1174 } 1175 1176 @Override 1177 public void destroy() { 1178 synchronized (mLock) { 1179 mCallback = null; 1180 mPendingCallback = null; 1181 } 1182 } 1183 } 1184 1185 private final class MyWriteResultCallback extends WriteResultCallback 1186 implements DestroyableCallback { 1187 private ParcelFileDescriptor mFd; 1188 private IWriteResultCallback mCallback; 1189 private final int mSequence; 1190 1191 public MyWriteResultCallback(IWriteResultCallback callback, 1192 ParcelFileDescriptor fd, int sequence) { 1193 mFd = fd; 1194 mSequence = sequence; 1195 mCallback = callback; 1196 } 1197 1198 @Override 1199 public void onWriteFinished(PageRange[] pages) { 1200 final IWriteResultCallback callback; 1201 synchronized (mLock) { 1202 callback = mCallback; 1203 } 1204 1205 // If the callback is null we are destroyed. 1206 if (callback == null) { 1207 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1208 + "finish the printing activity before print completion " 1209 + "or did you invoke a callback after finish?"); 1210 return; 1211 } 1212 1213 try { 1214 if (pages == null) { 1215 throw new IllegalArgumentException("pages cannot be null"); 1216 } 1217 if (pages.length == 0) { 1218 throw new IllegalArgumentException("pages cannot be empty"); 1219 } 1220 1221 try { 1222 callback.onWriteFinished(pages, mSequence); 1223 } catch (RemoteException re) { 1224 Log.e(LOG_TAG, "Error calling onWriteFinished", re); 1225 } 1226 } finally { 1227 destroy(); 1228 } 1229 } 1230 1231 @Override 1232 public void onWriteFailed(CharSequence error) { 1233 final IWriteResultCallback callback; 1234 synchronized (mLock) { 1235 callback = mCallback; 1236 } 1237 1238 // If the callback is null we are destroyed. 1239 if (callback == null) { 1240 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1241 + "finish the printing activity before print completion " 1242 + "or did you invoke a callback after finish?"); 1243 return; 1244 } 1245 1246 try { 1247 callback.onWriteFailed(error, mSequence); 1248 } catch (RemoteException re) { 1249 Log.e(LOG_TAG, "Error calling onWriteFailed", re); 1250 } finally { 1251 destroy(); 1252 } 1253 } 1254 1255 @Override 1256 public void onWriteCancelled() { 1257 final IWriteResultCallback callback; 1258 synchronized (mLock) { 1259 callback = mCallback; 1260 } 1261 1262 // If the callback is null we are destroyed. 1263 if (callback == null) { 1264 Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you " 1265 + "finish the printing activity before print completion " 1266 + "or did you invoke a callback after finish?"); 1267 return; 1268 } 1269 1270 try { 1271 callback.onWriteCanceled(mSequence); 1272 } catch (RemoteException re) { 1273 Log.e(LOG_TAG, "Error calling onWriteCanceled", re); 1274 } finally { 1275 destroy(); 1276 } 1277 } 1278 1279 @Override 1280 public void destroy() { 1281 synchronized (mLock) { 1282 IoUtils.closeQuietly(mFd); 1283 mCallback = null; 1284 mFd = null; 1285 mPendingCallback = null; 1286 } 1287 } 1288 } 1289 } 1290 1291 /** 1292 * @hide 1293 */ 1294 public static final class PrintJobStateChangeListenerWrapper extends 1295 IPrintJobStateChangeListener.Stub { 1296 private final WeakReference<PrintJobStateChangeListener> mWeakListener; 1297 private final WeakReference<Handler> mWeakHandler; 1298 1299 public PrintJobStateChangeListenerWrapper(PrintJobStateChangeListener listener, 1300 Handler handler) { 1301 mWeakListener = new WeakReference<PrintJobStateChangeListener>(listener); 1302 mWeakHandler = new WeakReference<Handler>(handler); 1303 } 1304 1305 @Override 1306 public void onPrintJobStateChanged(PrintJobId printJobId) { 1307 Handler handler = mWeakHandler.get(); 1308 PrintJobStateChangeListener listener = mWeakListener.get(); 1309 if (handler != null && listener != null) { 1310 SomeArgs args = SomeArgs.obtain(); 1311 args.arg1 = this; 1312 args.arg2 = printJobId; 1313 handler.obtainMessage(MSG_NOTIFY_PRINT_JOB_STATE_CHANGED, 1314 args).sendToTarget(); 1315 } 1316 } 1317 1318 public void destroy() { 1319 mWeakListener.clear(); 1320 } 1321 1322 public PrintJobStateChangeListener getListener() { 1323 return mWeakListener.get(); 1324 } 1325 } 1326 1327 /** 1328 * @hide 1329 */ 1330 public static final class PrintServicesChangeListenerWrapper extends 1331 IPrintServicesChangeListener.Stub { 1332 private final WeakReference<PrintServicesChangeListener> mWeakListener; 1333 private final WeakReference<Handler> mWeakHandler; 1334 1335 public PrintServicesChangeListenerWrapper(PrintServicesChangeListener listener, 1336 Handler handler) { 1337 mWeakListener = new WeakReference<>(listener); 1338 mWeakHandler = new WeakReference<>(handler); 1339 } 1340 1341 @Override 1342 public void onPrintServicesChanged() { 1343 Handler handler = mWeakHandler.get(); 1344 PrintServicesChangeListener listener = mWeakListener.get(); 1345 if (handler != null && listener != null) { 1346 handler.obtainMessage(MSG_NOTIFY_PRINT_SERVICES_CHANGED, this).sendToTarget(); 1347 } 1348 } 1349 1350 public void destroy() { 1351 mWeakListener.clear(); 1352 } 1353 1354 public PrintServicesChangeListener getListener() { 1355 return mWeakListener.get(); 1356 } 1357 } 1358 1359 /** 1360 * @hide 1361 */ 1362 public static final class PrintServiceRecommendationsChangeListenerWrapper extends 1363 IRecommendationsChangeListener.Stub { 1364 private final WeakReference<PrintServiceRecommendationsChangeListener> mWeakListener; 1365 private final WeakReference<Handler> mWeakHandler; 1366 1367 public PrintServiceRecommendationsChangeListenerWrapper( 1368 PrintServiceRecommendationsChangeListener listener, Handler handler) { 1369 mWeakListener = new WeakReference<>(listener); 1370 mWeakHandler = new WeakReference<>(handler); 1371 } 1372 1373 @Override 1374 public void onRecommendationsChanged() { 1375 Handler handler = mWeakHandler.get(); 1376 PrintServiceRecommendationsChangeListener listener = mWeakListener.get(); 1377 if (handler != null && listener != null) { 1378 handler.obtainMessage(MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED, 1379 this).sendToTarget(); 1380 } 1381 } 1382 1383 public void destroy() { 1384 mWeakListener.clear(); 1385 } 1386 1387 public PrintServiceRecommendationsChangeListener getListener() { 1388 return mWeakListener.get(); 1389 } 1390 } 1391} 1392