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