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