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