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