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