1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v4.app;
18 
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.Service;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.ServiceConnection;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.DeadObjectException;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 import android.os.IBinder;
34 import android.os.Message;
35 import android.os.RemoteException;
36 import android.provider.Settings;
37 import android.util.Log;
38 
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.Iterator;
42 import java.util.LinkedList;
43 import java.util.List;
44 import java.util.Map;
45 import 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  */
54 public 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. */
from(Context context)102     public static NotificationManagerCompat from(Context context) {
103         return new NotificationManagerCompat(context);
104     }
105 
NotificationManagerCompat(Context context)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 {
cancelNotification(NotificationManager notificationManager, String tag, int id)115         void cancelNotification(NotificationManager notificationManager, String tag, int id);
116 
postNotification(NotificationManager notificationManager, String tag, int id, Notification notification)117         void postNotification(NotificationManager notificationManager, String tag, int id,
118                 Notification notification);
119 
getSideChannelBindFlags()120         int getSideChannelBindFlags();
121     }
122 
123     static class ImplBase implements Impl {
124         @Override
cancelNotification(NotificationManager notificationManager, String tag, int id)125         public void cancelNotification(NotificationManager notificationManager, String tag,
126                 int id) {
127             notificationManager.cancel(id);
128         }
129 
130         @Override
postNotification(NotificationManager notificationManager, String tag, int id, Notification notification)131         public void postNotification(NotificationManager notificationManager, String tag, int id,
132                 Notification notification) {
133             notificationManager.notify(id, notification);
134         }
135 
136         @Override
getSideChannelBindFlags()137         public int getSideChannelBindFlags() {
138             return Service.BIND_AUTO_CREATE;
139         }
140     }
141 
142     static class ImplEclair extends ImplBase {
143         @Override
cancelNotification(NotificationManager notificationManager, String tag, int id)144         public void cancelNotification(NotificationManager notificationManager, String tag,
145                 int id) {
146             NotificationManagerCompatEclair.cancelNotification(notificationManager, tag, id);
147         }
148 
149         @Override
postNotification(NotificationManager notificationManager, String tag, int id, Notification notification)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
getSideChannelBindFlags()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      */
cancel(int id)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      */
cancel(String tag, int id)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. */
cancelAll()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      */
notify(int id, Notification notification)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     */
notify(String tag, int id, Notification notification)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      */
getEnabledListenerPackages(Context context)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      */
useSideChannelForNotification(Notification notification)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      */
pushSideChannelQueue(Task task)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 
SideChannelManager(Context context)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          */
queueTask(Task task)305         public void queueTask(Task task) {
306             mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget();
307         }
308 
309         @Override
handleMessage(Message msg)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 
handleQueueTask(Task task)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 
handleServiceConnected(ComponentName componentName, IBinder iBinder)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 
handleServiceDisconnected(ComponentName componentName)346         private void handleServiceDisconnected(ComponentName componentName) {
347             ListenerRecord record = mRecordMap.get(componentName);
348             if (record != null) {
349                 ensureServiceUnbound(record);
350             }
351         }
352 
handleRetryListenerQueue(ComponentName componentName)353         private void handleRetryListenerQueue(ComponentName componentName) {
354             ListenerRecord record = mRecordMap.get(componentName);
355             if (record != null) {
356                 processListenerQueue(record);
357             }
358         }
359 
360         @Override
onServiceConnected(ComponentName componentName, IBinder iBinder)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
onServiceDisconnected(ComponentName componentName)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          */
updateListenerMap()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          */
ensureServiceBound(ListenerRecord record)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          */
ensureServiceUnbound(ListenerRecord record)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          */
scheduleListenerRetry(ListenerRecord record)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          */
processListenerQueue(ListenerRecord record)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 
ListenerRecord(ComponentName componentName)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 
ServiceConnectedEvent(ComponentName componentName, final IBinder iBinder)550         public ServiceConnectedEvent(ComponentName componentName,
551                 final IBinder iBinder) {
552             this.componentName = componentName;
553             this.iBinder = iBinder;
554         }
555     }
556 
557     private interface Task {
send(INotificationSideChannel service)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 
NotifyTask(String packageName, int id, String tag, Notification notif)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
send(INotificationSideChannel service)575         public void send(INotificationSideChannel service) throws RemoteException {
576             service.notify(packageName, id, tag, notif);
577         }
578 
toString()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 
CancelTask(String packageName)595         public CancelTask(String packageName) {
596             this.packageName = packageName;
597             this.id = 0;
598             this.tag = null;
599             this.all = true;
600         }
601 
CancelTask(String packageName, int id, String tag)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
send(INotificationSideChannel service)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 
toString()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