NotificationManagerCompat.java revision 1a7f163fad9e7d0f5bc67ad44d6bf9d73d672a86
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 /** 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