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