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