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