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