1/* 2 * Copyright (C) 2014 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.support.v4.app; 18 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.Service; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.ServiceConnection; 26import android.content.pm.PackageManager; 27import android.content.pm.ResolveInfo; 28import android.os.Build; 29import android.os.Bundle; 30import android.os.DeadObjectException; 31import android.os.Handler; 32import android.os.HandlerThread; 33import android.os.IBinder; 34import android.os.Message; 35import android.os.RemoteException; 36import android.provider.Settings; 37import android.support.v4.os.BuildCompat; 38import android.util.Log; 39 40import java.util.HashMap; 41import java.util.HashSet; 42import java.util.Iterator; 43import java.util.LinkedList; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47 48/** 49 * Compatibility library for NotificationManager with fallbacks for older platforms. 50 * 51 * <p>To use this class, call the static function {@link #from} to get a 52 * {@link NotificationManagerCompat} object, and then call one of its 53 * methods to post or cancel notifications. 54 */ 55public final class NotificationManagerCompat { 56 private static final String TAG = "NotifManCompat"; 57 58 /** 59 * Notification extras key: if set to true, the posted notification should use 60 * the side channel for delivery instead of using notification manager. 61 */ 62 public static final String EXTRA_USE_SIDE_CHANNEL = 63 NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL; 64 65 /** 66 * Intent action to register for on a service to receive side channel 67 * notifications. The listening service must be in the same package as an enabled 68 * {@link android.service.notification.NotificationListenerService}. 69 */ 70 public static final String ACTION_BIND_SIDE_CHANNEL = 71 "android.support.BIND_NOTIFICATION_SIDE_CHANNEL"; 72 73 /** 74 * Maximum sdk build version which needs support for side channeled notifications. 75 * Currently the only needed use is for side channeling group children before KITKAT_WATCH. 76 */ 77 static final int MAX_SIDE_CHANNEL_SDK_VERSION = 19; 78 79 /** Base time delay for a side channel listener queue retry. */ 80 private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000; 81 /** Maximum retries for a side channel listener before dropping tasks. */ 82 private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6; 83 /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ 84 private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS = 85 "enabled_notification_listeners"; 86 private static final int SIDE_CHANNEL_BIND_FLAGS; 87 88 /** Cache of enabled notification listener components */ 89 private static final Object sEnabledNotificationListenersLock = new Object(); 90 /** Guarded by {@link #sEnabledNotificationListenersLock} */ 91 private static String sEnabledNotificationListeners; 92 /** Guarded by {@link #sEnabledNotificationListenersLock} */ 93 private static Set<String> sEnabledNotificationListenerPackages = new HashSet<String>(); 94 95 private final Context mContext; 96 private final NotificationManager mNotificationManager; 97 /** Lock for mutable static fields */ 98 private static final Object sLock = new Object(); 99 /** Guarded by {@link #sLock} */ 100 private static SideChannelManager sSideChannelManager; 101 102 /** 103 * Value signifying that the user has not expressed an importance. 104 * 105 * This value is for persisting preferences, and should never be associated with 106 * an actual notification. 107 */ 108 public static final int IMPORTANCE_UNSPECIFIED = -1000; 109 110 /** 111 * A notification with no importance: shows nowhere, is blocked. 112 */ 113 public static final int IMPORTANCE_NONE = 0; 114 115 /** 116 * Min notification importance: only shows in the shade, below the fold. 117 */ 118 public static final int IMPORTANCE_MIN = 1; 119 120 /** 121 * Low notification importance: shows everywhere, but is not intrusive. 122 */ 123 public static final int IMPORTANCE_LOW = 2; 124 125 /** 126 * Default notification importance: shows everywhere, allowed to makes noise, 127 * but does not visually intrude. 128 */ 129 public static final int IMPORTANCE_DEFAULT = 3; 130 131 /** 132 * Higher notification importance: shows everywhere, allowed to makes noise and peek. 133 */ 134 public static final int IMPORTANCE_HIGH = 4; 135 136 /** 137 * Highest notification importance: shows everywhere, allowed to makes noise, peek, and 138 * use full screen intents. 139 */ 140 public static final int IMPORTANCE_MAX = 5; 141 142 /** Get a {@link NotificationManagerCompat} instance for a provided context. */ 143 public static NotificationManagerCompat from(Context context) { 144 return new NotificationManagerCompat(context); 145 } 146 147 private NotificationManagerCompat(Context context) { 148 mContext = context; 149 mNotificationManager = (NotificationManager) mContext.getSystemService( 150 Context.NOTIFICATION_SERVICE); 151 } 152 153 private static final Impl IMPL; 154 155 interface Impl { 156 void cancelNotification(NotificationManager notificationManager, String tag, int id); 157 158 void postNotification(NotificationManager notificationManager, String tag, int id, 159 Notification notification); 160 161 int getSideChannelBindFlags(); 162 163 boolean areNotificationsEnabled(Context context, NotificationManager notificationManager); 164 165 int getImportance(NotificationManager notificationManager); 166 } 167 168 static class ImplBase implements Impl { 169 170 @Override 171 public void cancelNotification(NotificationManager notificationManager, String tag, 172 int id) { 173 notificationManager.cancel(id); 174 } 175 176 @Override 177 public void postNotification(NotificationManager notificationManager, String tag, int id, 178 Notification notification) { 179 notificationManager.notify(id, notification); 180 } 181 182 @Override 183 public int getSideChannelBindFlags() { 184 return Service.BIND_AUTO_CREATE; 185 } 186 187 @Override 188 public boolean areNotificationsEnabled(Context context, 189 NotificationManager notificationManager) { 190 return true; 191 } 192 193 @Override 194 public int getImportance(NotificationManager notificationManager) { 195 return IMPORTANCE_UNSPECIFIED; 196 } 197 } 198 199 static class ImplEclair extends ImplBase { 200 @Override 201 public void cancelNotification(NotificationManager notificationManager, String tag, 202 int id) { 203 NotificationManagerCompatEclair.cancelNotification(notificationManager, tag, id); 204 } 205 206 @Override 207 public void postNotification(NotificationManager notificationManager, String tag, int id, 208 Notification notification) { 209 NotificationManagerCompatEclair.postNotification(notificationManager, tag, id, 210 notification); 211 } 212 } 213 214 static class ImplIceCreamSandwich extends ImplEclair { 215 @Override 216 public int getSideChannelBindFlags() { 217 return NotificationManagerCompatIceCreamSandwich.SIDE_CHANNEL_BIND_FLAGS; 218 } 219 } 220 221 static class ImplKitKat extends ImplIceCreamSandwich { 222 @Override 223 public boolean areNotificationsEnabled(Context context, 224 NotificationManager notificationManager) { 225 return NotificationManagerCompatKitKat.areNotificationsEnabled(context); 226 } 227 } 228 229 static class ImplApi24 extends ImplKitKat { 230 @Override 231 public boolean areNotificationsEnabled(Context context, 232 NotificationManager notificationManager) { 233 return NotificationManagerCompatApi24.areNotificationsEnabled(notificationManager); 234 } 235 236 @Override 237 public int getImportance(NotificationManager notificationManager) { 238 return NotificationManagerCompatApi24.getImportance(notificationManager); 239 } 240 } 241 242 static { 243 if (BuildCompat.isAtLeastN()) { 244 IMPL = new ImplApi24(); 245 } else if (Build.VERSION.SDK_INT >= 19) { 246 IMPL = new ImplKitKat(); 247 } else if (Build.VERSION.SDK_INT >= 14) { 248 IMPL = new ImplIceCreamSandwich(); 249 } else if (Build.VERSION.SDK_INT >= 5) { 250 IMPL = new ImplEclair(); 251 } else { 252 IMPL = new ImplBase(); 253 } 254 SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags(); 255 } 256 257 /** 258 * Cancel a previously shown notification. 259 * @param id the ID of the notification 260 */ 261 public void cancel(int id) { 262 cancel(null, id); 263 } 264 265 /** 266 * Cancel a previously shown notification. 267 * @param tag the string identifier of the notification. 268 * @param id the ID of the notification 269 */ 270 public void cancel(String tag, int id) { 271 IMPL.cancelNotification(mNotificationManager, tag, id); 272 if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { 273 pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag)); 274 } 275 } 276 277 /** Cancel all previously shown notifications. */ 278 public void cancelAll() { 279 mNotificationManager.cancelAll(); 280 if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { 281 pushSideChannelQueue(new CancelTask(mContext.getPackageName())); 282 } 283 } 284 285 /** 286 * Post a notification to be shown in the status bar, stream, etc. 287 * @param id the ID of the notification 288 * @param notification the notification to post to the system 289 */ 290 public void notify(int id, Notification notification) { 291 notify(null, id, notification); 292 } 293 294 /** 295 * Post a notification to be shown in the status bar, stream, etc. 296 * @param tag the string identifier for a notification. Can be {@code null}. 297 * @param id the ID of the notification. The pair (tag, id) must be unique within your app. 298 * @param notification the notification to post to the system 299 */ 300 public void notify(String tag, int id, Notification notification) { 301 if (useSideChannelForNotification(notification)) { 302 pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); 303 // Cancel this notification in notification manager if it just transitioned to being 304 // side channelled. 305 IMPL.cancelNotification(mNotificationManager, tag, id); 306 } else { 307 IMPL.postNotification(mNotificationManager, tag, id, notification); 308 } 309 } 310 311 /** 312 * Returns whether notifications from the calling package are not blocked. 313 */ 314 public boolean areNotificationsEnabled() { 315 return IMPL.areNotificationsEnabled(mContext, mNotificationManager); 316 } 317 318 /** 319 * Returns the user specified importance for notifications from the calling package. 320 * 321 * @return An importance level, such as {@link #IMPORTANCE_DEFAULT}. 322 */ 323 public int getImportance() { 324 return IMPL.getImportance(mNotificationManager); 325 } 326 327 /** 328 * Get the set of packages that have an enabled notification listener component within them. 329 */ 330 public static Set<String> getEnabledListenerPackages(Context context) { 331 final String enabledNotificationListeners = Settings.Secure.getString( 332 context.getContentResolver(), 333 SETTING_ENABLED_NOTIFICATION_LISTENERS); 334 synchronized (sEnabledNotificationListenersLock) { 335 // Parse the string again if it is different from the last time this method was called. 336 if (enabledNotificationListeners != null 337 && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) { 338 final String[] components = enabledNotificationListeners.split(":"); 339 Set<String> packageNames = new HashSet<String>(components.length); 340 for (String component : components) { 341 ComponentName componentName = ComponentName.unflattenFromString(component); 342 if (componentName != null) { 343 packageNames.add(componentName.getPackageName()); 344 } 345 } 346 sEnabledNotificationListenerPackages = packageNames; 347 sEnabledNotificationListeners = enabledNotificationListeners; 348 } 349 return sEnabledNotificationListenerPackages; 350 } 351 } 352 353 /** 354 * Returns true if this notification should use the side channel for delivery. 355 */ 356 private static boolean useSideChannelForNotification(Notification notification) { 357 Bundle extras = NotificationCompat.getExtras(notification); 358 return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL); 359 } 360 361 /** 362 * Push a notification task for distribution to notification side channels. 363 */ 364 private void pushSideChannelQueue(Task task) { 365 synchronized (sLock) { 366 if (sSideChannelManager == null) { 367 sSideChannelManager = new SideChannelManager(mContext.getApplicationContext()); 368 } 369 sSideChannelManager.queueTask(task); 370 } 371 } 372 373 /** 374 * Helper class to manage a queue of pending tasks to send to notification side channel 375 * listeners. 376 */ 377 private static class SideChannelManager implements Handler.Callback, ServiceConnection { 378 private static final int MSG_QUEUE_TASK = 0; 379 private static final int MSG_SERVICE_CONNECTED = 1; 380 private static final int MSG_SERVICE_DISCONNECTED = 2; 381 private static final int MSG_RETRY_LISTENER_QUEUE = 3; 382 383 private static final String KEY_BINDER = "binder"; 384 385 private final Context mContext; 386 private final HandlerThread mHandlerThread; 387 private final Handler mHandler; 388 private final Map<ComponentName, ListenerRecord> mRecordMap = 389 new HashMap<ComponentName, ListenerRecord>(); 390 private Set<String> mCachedEnabledPackages = new HashSet<String>(); 391 392 public SideChannelManager(Context context) { 393 mContext = context; 394 mHandlerThread = new HandlerThread("NotificationManagerCompat"); 395 mHandlerThread.start(); 396 mHandler = new Handler(mHandlerThread.getLooper(), this); 397 } 398 399 /** 400 * Queue a new task to be sent to all listeners. This function can be called 401 * from any thread. 402 */ 403 public void queueTask(Task task) { 404 mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget(); 405 } 406 407 @Override 408 public boolean handleMessage(Message msg) { 409 switch (msg.what) { 410 case MSG_QUEUE_TASK: 411 handleQueueTask((Task) msg.obj); 412 return true; 413 case MSG_SERVICE_CONNECTED: 414 ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj; 415 handleServiceConnected(event.componentName, event.iBinder); 416 return true; 417 case MSG_SERVICE_DISCONNECTED: 418 handleServiceDisconnected((ComponentName) msg.obj); 419 return true; 420 case MSG_RETRY_LISTENER_QUEUE: 421 handleRetryListenerQueue((ComponentName) msg.obj); 422 return true; 423 } 424 return false; 425 } 426 427 private void handleQueueTask(Task task) { 428 updateListenerMap(); 429 for (ListenerRecord record : mRecordMap.values()) { 430 record.taskQueue.add(task); 431 processListenerQueue(record); 432 } 433 } 434 435 private void handleServiceConnected(ComponentName componentName, IBinder iBinder) { 436 ListenerRecord record = mRecordMap.get(componentName); 437 if (record != null) { 438 record.service = INotificationSideChannel.Stub.asInterface(iBinder); 439 record.retryCount = 0; 440 processListenerQueue(record); 441 } 442 } 443 444 private void handleServiceDisconnected(ComponentName componentName) { 445 ListenerRecord record = mRecordMap.get(componentName); 446 if (record != null) { 447 ensureServiceUnbound(record); 448 } 449 } 450 451 private void handleRetryListenerQueue(ComponentName componentName) { 452 ListenerRecord record = mRecordMap.get(componentName); 453 if (record != null) { 454 processListenerQueue(record); 455 } 456 } 457 458 @Override 459 public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 460 if (Log.isLoggable(TAG, Log.DEBUG)) { 461 Log.d(TAG, "Connected to service " + componentName); 462 } 463 mHandler.obtainMessage(MSG_SERVICE_CONNECTED, 464 new ServiceConnectedEvent(componentName, iBinder)) 465 .sendToTarget(); 466 } 467 468 @Override 469 public void onServiceDisconnected(ComponentName componentName) { 470 if (Log.isLoggable(TAG, Log.DEBUG)) { 471 Log.d(TAG, "Disconnected from service " + componentName); 472 } 473 mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget(); 474 } 475 476 /** 477 * Check the current list of enabled listener packages and update the records map 478 * accordingly. 479 */ 480 private void updateListenerMap() { 481 Set<String> enabledPackages = getEnabledListenerPackages(mContext); 482 if (enabledPackages.equals(mCachedEnabledPackages)) { 483 // Short-circuit when the list of enabled packages has not changed. 484 return; 485 } 486 mCachedEnabledPackages = enabledPackages; 487 List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices( 488 new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES); 489 Set<ComponentName> enabledComponents = new HashSet<ComponentName>(); 490 for (ResolveInfo resolveInfo : resolveInfos) { 491 if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) { 492 continue; 493 } 494 ComponentName componentName = new ComponentName( 495 resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); 496 if (resolveInfo.serviceInfo.permission != null) { 497 Log.w(TAG, "Permission present on component " + componentName 498 + ", not adding listener record."); 499 continue; 500 } 501 enabledComponents.add(componentName); 502 } 503 // Ensure all enabled components have a record in the listener map. 504 for (ComponentName componentName : enabledComponents) { 505 if (!mRecordMap.containsKey(componentName)) { 506 if (Log.isLoggable(TAG, Log.DEBUG)) { 507 Log.d(TAG, "Adding listener record for " + componentName); 508 } 509 mRecordMap.put(componentName, new ListenerRecord(componentName)); 510 } 511 } 512 // Remove listener records that are no longer for enabled components. 513 Iterator<Map.Entry<ComponentName, ListenerRecord>> it = 514 mRecordMap.entrySet().iterator(); 515 while (it.hasNext()) { 516 Map.Entry<ComponentName, ListenerRecord> entry = it.next(); 517 if (!enabledComponents.contains(entry.getKey())) { 518 if (Log.isLoggable(TAG, Log.DEBUG)) { 519 Log.d(TAG, "Removing listener record for " + entry.getKey()); 520 } 521 ensureServiceUnbound(entry.getValue()); 522 it.remove(); 523 } 524 } 525 } 526 527 /** 528 * Ensure we are already attempting to bind to a service, or start a new binding if not. 529 * @return Whether the service bind attempt was successful. 530 */ 531 private boolean ensureServiceBound(ListenerRecord record) { 532 if (record.bound) { 533 return true; 534 } 535 Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName); 536 record.bound = mContext.bindService(intent, this, SIDE_CHANNEL_BIND_FLAGS); 537 if (record.bound) { 538 record.retryCount = 0; 539 } else { 540 Log.w(TAG, "Unable to bind to listener " + record.componentName); 541 mContext.unbindService(this); 542 } 543 return record.bound; 544 } 545 546 /** 547 * Ensure we have unbound from a service. 548 */ 549 private void ensureServiceUnbound(ListenerRecord record) { 550 if (record.bound) { 551 mContext.unbindService(this); 552 record.bound = false; 553 } 554 record.service = null; 555 } 556 557 /** 558 * Schedule a delayed retry to communicate with a listener service. 559 * After a maximum number of attempts (with exponential back-off), start 560 * dropping pending tasks for this listener. 561 */ 562 private void scheduleListenerRetry(ListenerRecord record) { 563 if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) { 564 return; 565 } 566 record.retryCount++; 567 if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) { 568 Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to " 569 + record.componentName + " after " + record.retryCount + " retries"); 570 record.taskQueue.clear(); 571 return; 572 } 573 int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1)); 574 if (Log.isLoggable(TAG, Log.DEBUG)) { 575 Log.d(TAG, "Scheduling retry for " + delayMs + " ms"); 576 } 577 Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName); 578 mHandler.sendMessageDelayed(msg, delayMs); 579 } 580 581 /** 582 * Perform a processing step for a listener. First check the bind state, then attempt 583 * to flush the task queue, and if an error is encountered, schedule a retry. 584 */ 585 private void processListenerQueue(ListenerRecord record) { 586 if (Log.isLoggable(TAG, Log.DEBUG)) { 587 Log.d(TAG, "Processing component " + record.componentName + ", " 588 + record.taskQueue.size() + " queued tasks"); 589 } 590 if (record.taskQueue.isEmpty()) { 591 return; 592 } 593 if (!ensureServiceBound(record) || record.service == null) { 594 // Ensure bind has started and that a service interface is ready to use. 595 scheduleListenerRetry(record); 596 return; 597 } 598 // Attempt to flush all items in the task queue. 599 while (true) { 600 Task task = record.taskQueue.peek(); 601 if (task == null) { 602 break; 603 } 604 try { 605 if (Log.isLoggable(TAG, Log.DEBUG)) { 606 Log.d(TAG, "Sending task " + task); 607 } 608 task.send(record.service); 609 record.taskQueue.remove(); 610 } catch (DeadObjectException e) { 611 if (Log.isLoggable(TAG, Log.DEBUG)) { 612 Log.d(TAG, "Remote service has died: " + record.componentName); 613 } 614 break; 615 } catch (RemoteException e) { 616 Log.w(TAG, "RemoteException communicating with " + record.componentName, e); 617 break; 618 } 619 } 620 if (!record.taskQueue.isEmpty()) { 621 // Some tasks were not sent, meaning an error was encountered, schedule a retry. 622 scheduleListenerRetry(record); 623 } 624 } 625 626 /** A per-side-channel-service listener state record */ 627 private static class ListenerRecord { 628 public final ComponentName componentName; 629 /** Whether the service is currently bound to. */ 630 public boolean bound = false; 631 /** The service stub provided by onServiceConnected */ 632 public INotificationSideChannel service; 633 /** Queue of pending tasks to send to this listener service */ 634 public LinkedList<Task> taskQueue = new LinkedList<Task>(); 635 /** Number of retries attempted while connecting to this listener service */ 636 public int retryCount = 0; 637 638 public ListenerRecord(ComponentName componentName) { 639 this.componentName = componentName; 640 } 641 } 642 } 643 644 private static class ServiceConnectedEvent { 645 final ComponentName componentName; 646 final IBinder iBinder; 647 648 public ServiceConnectedEvent(ComponentName componentName, 649 final IBinder iBinder) { 650 this.componentName = componentName; 651 this.iBinder = iBinder; 652 } 653 } 654 655 private interface Task { 656 public void send(INotificationSideChannel service) throws RemoteException; 657 } 658 659 private static class NotifyTask implements Task { 660 final String packageName; 661 final int id; 662 final String tag; 663 final Notification notif; 664 665 public NotifyTask(String packageName, int id, String tag, Notification notif) { 666 this.packageName = packageName; 667 this.id = id; 668 this.tag = tag; 669 this.notif = notif; 670 } 671 672 @Override 673 public void send(INotificationSideChannel service) throws RemoteException { 674 service.notify(packageName, id, tag, notif); 675 } 676 677 public String toString() { 678 StringBuilder sb = new StringBuilder("NotifyTask["); 679 sb.append("packageName:").append(packageName); 680 sb.append(", id:").append(id); 681 sb.append(", tag:").append(tag); 682 sb.append("]"); 683 return sb.toString(); 684 } 685 } 686 687 private static class CancelTask implements Task { 688 final String packageName; 689 final int id; 690 final String tag; 691 final boolean all; 692 693 public CancelTask(String packageName) { 694 this.packageName = packageName; 695 this.id = 0; 696 this.tag = null; 697 this.all = true; 698 } 699 700 public CancelTask(String packageName, int id, String tag) { 701 this.packageName = packageName; 702 this.id = id; 703 this.tag = tag; 704 this.all = false; 705 } 706 707 @Override 708 public void send(INotificationSideChannel service) throws RemoteException { 709 if (all) { 710 service.cancelAll(packageName); 711 } else { 712 service.cancel(packageName, id, tag); 713 } 714 } 715 716 public String toString() { 717 StringBuilder sb = new StringBuilder("CancelTask["); 718 sb.append("packageName:").append(packageName); 719 sb.append(", id:").append(id); 720 sb.append(", tag:").append(tag); 721 sb.append(", all:").append(all); 722 sb.append("]"); 723 return sb.toString(); 724 } 725 } 726} 727