1 /* 2 * Copyright (C) 2015 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.service.notification; 18 19 import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SdkConstant; 25 import android.annotation.SystemApi; 26 import android.annotation.TestApi; 27 import android.app.Notification; 28 import android.app.NotificationChannel; 29 import android.app.NotificationManager; 30 import android.app.admin.DevicePolicyManager; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.os.Message; 38 import android.os.RemoteException; 39 import android.util.Log; 40 41 import com.android.internal.os.SomeArgs; 42 43 import java.lang.annotation.Retention; 44 import java.util.List; 45 46 /** 47 * A service that helps the user manage notifications. 48 * <p> 49 * Only one notification assistant can be active at a time. Unlike notification listener services, 50 * assistant services can additionally modify certain aspects about notifications 51 * (see {@link Adjustment}) before they are posted. 52 *<p> 53 * A note about managed profiles: Unlike {@link NotificationListenerService listener services}, 54 * NotificationAssistantServices are allowed to run in managed profiles 55 * (see {@link DevicePolicyManager#isManagedProfile(ComponentName)}), so they can access the 56 * information they need to create good {@link Adjustment adjustments}. To maintain the contract 57 * with {@link NotificationListenerService}, an assistant service will receive all of the 58 * callbacks from {@link NotificationListenerService} for the current user, managed profiles of 59 * that user, and ones that affect all users. However, 60 * {@link #onNotificationEnqueued(StatusBarNotification)} will only be called for notifications 61 * sent to the current user, and {@link Adjustment adjuments} will only be accepted for the 62 * current user. 63 * <p> 64 * All callbacks are called on the main thread. 65 * </p> 66 * @hide 67 */ 68 @SystemApi 69 @TestApi 70 public abstract class NotificationAssistantService extends NotificationListenerService { 71 private static final String TAG = "NotificationAssistants"; 72 73 /** @hide */ 74 @Retention(SOURCE) 75 @IntDef({SOURCE_FROM_APP, SOURCE_FROM_ASSISTANT}) 76 public @interface Source {} 77 78 /** 79 * To indicate an adjustment is from an app. 80 */ 81 public static final int SOURCE_FROM_APP = 0; 82 /** 83 * To indicate an adjustment is from a {@link NotificationAssistantService}. 84 */ 85 public static final int SOURCE_FROM_ASSISTANT = 1; 86 87 /** 88 * The {@link Intent} that must be declared as handled by the service. 89 */ 90 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 91 public static final String SERVICE_INTERFACE 92 = "android.service.notification.NotificationAssistantService"; 93 94 /** 95 * @hide 96 */ 97 protected Handler mHandler; 98 99 @Override attachBaseContext(Context base)100 protected void attachBaseContext(Context base) { 101 super.attachBaseContext(base); 102 mHandler = new MyHandler(getContext().getMainLooper()); 103 } 104 105 @Override onBind(@ullable Intent intent)106 public final @NonNull IBinder onBind(@Nullable Intent intent) { 107 if (mWrapper == null) { 108 mWrapper = new NotificationAssistantServiceWrapper(); 109 } 110 return mWrapper; 111 } 112 113 /** 114 * A notification was snoozed until a context. For use with 115 * {@link Adjustment#KEY_SNOOZE_CRITERIA}. When the device reaches the given context, the 116 * assistant should restore the notification with {@link #unsnoozeNotification(String)}. 117 * 118 * @param sbn the notification to snooze 119 * @param snoozeCriterionId the {@link SnoozeCriterion#getId()} representing a device context. 120 */ onNotificationSnoozedUntilContext(@onNull StatusBarNotification sbn, @NonNull String snoozeCriterionId)121 abstract public void onNotificationSnoozedUntilContext(@NonNull StatusBarNotification sbn, 122 @NonNull String snoozeCriterionId); 123 124 /** 125 * A notification was posted by an app. Called before post. 126 * 127 * <p>Note: this method is only called if you don't override 128 * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel)}.</p> 129 * 130 * @param sbn the new notification 131 * @return an adjustment or null to take no action, within 100ms. 132 */ onNotificationEnqueued(@onNull StatusBarNotification sbn)133 abstract public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn); 134 135 /** 136 * A notification was posted by an app. Called before post. 137 * 138 * @param sbn the new notification 139 * @param channel the channel the notification was posted to 140 * @return an adjustment or null to take no action, within 100ms. 141 */ onNotificationEnqueued(@onNull StatusBarNotification sbn, @NonNull NotificationChannel channel)142 public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn, 143 @NonNull NotificationChannel channel) { 144 return onNotificationEnqueued(sbn); 145 } 146 147 /** 148 * Implement this method to learn when notifications are removed, how they were interacted with 149 * before removal, and why they were removed. 150 * <p> 151 * This might occur because the user has dismissed the notification using system UI (or another 152 * notification listener) or because the app has withdrawn the notification. 153 * <p> 154 * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the 155 * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight 156 * fields such as {@link android.app.Notification#contentView} and 157 * {@link android.app.Notification#largeIcon}. However, all other fields on 158 * {@link StatusBarNotification}, sufficient to match this call with a prior call to 159 * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. 160 * 161 ** @param sbn A data structure encapsulating at least the original information (tag and id) 162 * and source (package name) used to post the {@link android.app.Notification} that 163 * was just removed. 164 * @param rankingMap The current ranking map that can be used to retrieve ranking information 165 * for active notifications. 166 * @param stats Stats about how the user interacted with the notification before it was removed. 167 * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. 168 */ 169 @Override onNotificationRemoved(@onNull StatusBarNotification sbn, @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason)170 public void onNotificationRemoved(@NonNull StatusBarNotification sbn, 171 @NonNull RankingMap rankingMap, 172 @NonNull NotificationStats stats, int reason) { 173 onNotificationRemoved(sbn, rankingMap, reason); 174 } 175 176 /** 177 * Implement this to know when a user has seen notifications, as triggered by 178 * {@link #setNotificationsShown(String[])}. 179 */ onNotificationsSeen(@onNull List<String> keys)180 public void onNotificationsSeen(@NonNull List<String> keys) { 181 182 } 183 184 /** 185 * Implement this to know when the notification panel is revealed 186 * 187 * @param items Number of notifications on the panel at time of opening 188 */ onPanelRevealed(int items)189 public void onPanelRevealed(int items) { 190 191 } 192 193 /** 194 * Implement this to know when the notification panel is hidden 195 */ onPanelHidden()196 public void onPanelHidden() { 197 198 } 199 200 /** 201 * Implement this to know when a notification becomes visible or hidden from the user. 202 * 203 * @param key the notification key 204 * @param isVisible whether the notification is visible. 205 */ onNotificationVisibilityChanged(@onNull String key, boolean isVisible)206 public void onNotificationVisibilityChanged(@NonNull String key, boolean isVisible) { 207 208 } 209 210 /** 211 * Implement this to know when a notification change (expanded / collapsed) is visible to user. 212 * 213 * @param key the notification key 214 * @param isUserAction whether the expanded change is caused by user action. 215 * @param isExpanded whether the notification is expanded. 216 */ onNotificationExpansionChanged( @onNull String key, boolean isUserAction, boolean isExpanded)217 public void onNotificationExpansionChanged( 218 @NonNull String key, boolean isUserAction, boolean isExpanded) {} 219 220 /** 221 * Implement this to know when a direct reply is sent from a notification. 222 * @param key the notification key 223 */ onNotificationDirectReplied(@onNull String key)224 public void onNotificationDirectReplied(@NonNull String key) {} 225 226 /** 227 * Implement this to know when a suggested reply is sent. 228 * @param key the notification key 229 * @param reply the reply that is just sent 230 * @param source the source that provided the reply, e.g. SOURCE_FROM_APP 231 */ onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, @Source int source)232 public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply, 233 @Source int source) { 234 } 235 236 /** 237 * Implement this to know when an action is clicked. 238 * @param key the notification key 239 * @param action the action that is just clicked 240 * @param source the source that provided the action, e.g. SOURCE_FROM_APP 241 */ onActionInvoked(@onNull String key, @NonNull Notification.Action action, @Source int source)242 public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action, 243 @Source int source) { 244 } 245 246 /** 247 * Implement this to know when a user has changed which features of 248 * their notifications the assistant can modify. 249 * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what 250 * {@link Adjustment adjustments} you are currently allowed to make.</p> 251 */ onAllowedAdjustmentsChanged()252 public void onAllowedAdjustmentsChanged() { 253 } 254 255 /** 256 * Updates a notification. N.B. this won’t cause 257 * an existing notification to alert, but might allow a future update to 258 * this notification to alert. 259 * 260 * @param adjustment the adjustment with an explanation 261 */ adjustNotification(@onNull Adjustment adjustment)262 public final void adjustNotification(@NonNull Adjustment adjustment) { 263 if (!isBound()) return; 264 try { 265 setAdjustmentIssuer(adjustment); 266 getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment); 267 } catch (android.os.RemoteException ex) { 268 Log.v(TAG, "Unable to contact notification manager", ex); 269 throw ex.rethrowFromSystemServer(); 270 } 271 } 272 273 /** 274 * Updates existing notifications. Re-ranking won't occur until all adjustments are applied. 275 * N.B. this won’t cause an existing notification to alert, but might allow a future update to 276 * these notifications to alert. 277 * 278 * @param adjustments a list of adjustments with explanations 279 */ adjustNotifications(@onNull List<Adjustment> adjustments)280 public final void adjustNotifications(@NonNull List<Adjustment> adjustments) { 281 if (!isBound()) return; 282 try { 283 for (Adjustment adjustment : adjustments) { 284 setAdjustmentIssuer(adjustment); 285 } 286 getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments); 287 } catch (android.os.RemoteException ex) { 288 Log.v(TAG, "Unable to contact notification manager", ex); 289 throw ex.rethrowFromSystemServer(); 290 } 291 } 292 293 /** 294 * Inform the notification manager about un-snoozing a specific notification. 295 * <p> 296 * This should only be used for notifications snoozed because of a contextual snooze suggestion 297 * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a 298 * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the 299 * notification. 300 * @param key The key of the notification to snooze 301 */ unsnoozeNotification(@onNull String key)302 public final void unsnoozeNotification(@NonNull String key) { 303 if (!isBound()) return; 304 try { 305 getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key); 306 } catch (android.os.RemoteException ex) { 307 Log.v(TAG, "Unable to contact notification manager", ex); 308 } 309 } 310 311 private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper { 312 @Override onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, NotificationChannel channel)313 public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, 314 NotificationChannel channel) { 315 StatusBarNotification sbn; 316 try { 317 sbn = sbnHolder.get(); 318 } catch (RemoteException e) { 319 Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e); 320 return; 321 } 322 if (sbn == null) { 323 Log.w(TAG, "onNotificationEnqueuedWithChannel: " 324 + "Error receiving StatusBarNotification"); 325 return; 326 } 327 328 SomeArgs args = SomeArgs.obtain(); 329 args.arg1 = sbn; 330 args.arg2 = channel; 331 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED, 332 args).sendToTarget(); 333 } 334 335 @Override onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)336 public void onNotificationSnoozedUntilContext( 337 IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) { 338 StatusBarNotification sbn; 339 try { 340 sbn = sbnHolder.get(); 341 } catch (RemoteException e) { 342 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e); 343 return; 344 } 345 if (sbn == null) { 346 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification"); 347 return; 348 } 349 350 SomeArgs args = SomeArgs.obtain(); 351 args.arg1 = sbn; 352 args.arg2 = snoozeCriterionId; 353 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED, 354 args).sendToTarget(); 355 } 356 357 @Override onNotificationsSeen(List<String> keys)358 public void onNotificationsSeen(List<String> keys) { 359 SomeArgs args = SomeArgs.obtain(); 360 args.arg1 = keys; 361 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN, 362 args).sendToTarget(); 363 } 364 365 @Override onPanelRevealed(int items)366 public void onPanelRevealed(int items) { 367 SomeArgs args = SomeArgs.obtain(); 368 args.argi1 = items; 369 mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_REVEALED, 370 args).sendToTarget(); 371 } 372 373 @Override onPanelHidden()374 public void onPanelHidden() { 375 SomeArgs args = SomeArgs.obtain(); 376 mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_HIDDEN, 377 args).sendToTarget(); 378 } 379 380 @Override onNotificationVisibilityChanged(String key, boolean isVisible)381 public void onNotificationVisibilityChanged(String key, boolean isVisible) { 382 SomeArgs args = SomeArgs.obtain(); 383 args.arg1 = key; 384 args.argi1 = isVisible ? 1 : 0; 385 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_VISIBILITY_CHANGED, 386 args).sendToTarget(); 387 } 388 389 @Override onNotificationExpansionChanged(String key, boolean isUserAction, boolean isExpanded)390 public void onNotificationExpansionChanged(String key, boolean isUserAction, 391 boolean isExpanded) { 392 SomeArgs args = SomeArgs.obtain(); 393 args.arg1 = key; 394 args.argi1 = isUserAction ? 1 : 0; 395 args.argi2 = isExpanded ? 1 : 0; 396 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args) 397 .sendToTarget(); 398 } 399 400 @Override onNotificationDirectReply(String key)401 public void onNotificationDirectReply(String key) { 402 SomeArgs args = SomeArgs.obtain(); 403 args.arg1 = key; 404 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args) 405 .sendToTarget(); 406 } 407 408 @Override onSuggestedReplySent(String key, CharSequence reply, int source)409 public void onSuggestedReplySent(String key, CharSequence reply, int source) { 410 SomeArgs args = SomeArgs.obtain(); 411 args.arg1 = key; 412 args.arg2 = reply; 413 args.argi2 = source; 414 mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget(); 415 } 416 417 @Override onActionClicked(String key, Notification.Action action, int source)418 public void onActionClicked(String key, Notification.Action action, int source) { 419 SomeArgs args = SomeArgs.obtain(); 420 args.arg1 = key; 421 args.arg2 = action; 422 args.argi2 = source; 423 mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget(); 424 } 425 426 @Override onAllowedAdjustmentsChanged()427 public void onAllowedAdjustmentsChanged() { 428 mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget(); 429 } 430 } 431 setAdjustmentIssuer(@ullable Adjustment adjustment)432 private void setAdjustmentIssuer(@Nullable Adjustment adjustment) { 433 if (adjustment != null) { 434 adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName()); 435 } 436 } 437 438 private final class MyHandler extends Handler { 439 public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1; 440 public static final int MSG_ON_NOTIFICATION_SNOOZED = 2; 441 public static final int MSG_ON_NOTIFICATIONS_SEEN = 3; 442 public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4; 443 public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5; 444 public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6; 445 public static final int MSG_ON_ACTION_INVOKED = 7; 446 public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8; 447 public static final int MSG_ON_PANEL_REVEALED = 9; 448 public static final int MSG_ON_PANEL_HIDDEN = 10; 449 public static final int MSG_ON_NOTIFICATION_VISIBILITY_CHANGED = 11; 450 MyHandler(Looper looper)451 public MyHandler(Looper looper) { 452 super(looper, null, false); 453 } 454 455 @Override handleMessage(Message msg)456 public void handleMessage(Message msg) { 457 switch (msg.what) { 458 case MSG_ON_NOTIFICATION_ENQUEUED: { 459 SomeArgs args = (SomeArgs) msg.obj; 460 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 461 NotificationChannel channel = (NotificationChannel) args.arg2; 462 args.recycle(); 463 Adjustment adjustment = onNotificationEnqueued(sbn, channel); 464 setAdjustmentIssuer(adjustment); 465 if (adjustment != null) { 466 if (!isBound()) { 467 Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip."); 468 return; 469 } 470 try { 471 getNotificationInterface().applyEnqueuedAdjustmentFromAssistant( 472 mWrapper, adjustment); 473 } catch (android.os.RemoteException ex) { 474 Log.v(TAG, "Unable to contact notification manager", ex); 475 throw ex.rethrowFromSystemServer(); 476 } catch (SecurityException e) { 477 // app cannot catch and recover from this, so do on their behalf 478 Log.w(TAG, "Enqueue adjustment failed; no longer connected", e); 479 } 480 } 481 break; 482 } 483 case MSG_ON_NOTIFICATION_SNOOZED: { 484 SomeArgs args = (SomeArgs) msg.obj; 485 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 486 String snoozeCriterionId = (String) args.arg2; 487 args.recycle(); 488 onNotificationSnoozedUntilContext(sbn, snoozeCriterionId); 489 break; 490 } 491 case MSG_ON_NOTIFICATIONS_SEEN: { 492 SomeArgs args = (SomeArgs) msg.obj; 493 List<String> keys = (List<String>) args.arg1; 494 args.recycle(); 495 onNotificationsSeen(keys); 496 break; 497 } 498 case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: { 499 SomeArgs args = (SomeArgs) msg.obj; 500 String key = (String) args.arg1; 501 boolean isUserAction = args.argi1 == 1; 502 boolean isExpanded = args.argi2 == 1; 503 args.recycle(); 504 onNotificationExpansionChanged(key, isUserAction, isExpanded); 505 break; 506 } 507 case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: { 508 SomeArgs args = (SomeArgs) msg.obj; 509 String key = (String) args.arg1; 510 args.recycle(); 511 onNotificationDirectReplied(key); 512 break; 513 } 514 case MSG_ON_SUGGESTED_REPLY_SENT: { 515 SomeArgs args = (SomeArgs) msg.obj; 516 String key = (String) args.arg1; 517 CharSequence reply = (CharSequence) args.arg2; 518 int source = args.argi2; 519 args.recycle(); 520 onSuggestedReplySent(key, reply, source); 521 break; 522 } 523 case MSG_ON_ACTION_INVOKED: { 524 SomeArgs args = (SomeArgs) msg.obj; 525 String key = (String) args.arg1; 526 Notification.Action action = (Notification.Action) args.arg2; 527 int source = args.argi2; 528 args.recycle(); 529 onActionInvoked(key, action, source); 530 break; 531 } 532 case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: { 533 onAllowedAdjustmentsChanged(); 534 break; 535 } 536 case MSG_ON_PANEL_REVEALED: { 537 SomeArgs args = (SomeArgs) msg.obj; 538 int items = args.argi1; 539 args.recycle(); 540 onPanelRevealed(items); 541 break; 542 } 543 case MSG_ON_PANEL_HIDDEN: { 544 onPanelHidden(); 545 break; 546 } 547 case MSG_ON_NOTIFICATION_VISIBILITY_CHANGED: { 548 SomeArgs args = (SomeArgs) msg.obj; 549 String key = (String) args.arg1; 550 boolean isVisible = args.argi1 == 1; 551 args.recycle(); 552 onNotificationVisibilityChanged(key, isVisible); 553 break; 554 } 555 } 556 } 557 } 558 } 559