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 a notification change (expanded / collapsed) is visible to user. 186 * 187 * @param key the notification key 188 * @param isUserAction whether the expanded change is caused by user action. 189 * @param isExpanded whether the notification is expanded. 190 */ onNotificationExpansionChanged( @onNull String key, boolean isUserAction, boolean isExpanded)191 public void onNotificationExpansionChanged( 192 @NonNull String key, boolean isUserAction, boolean isExpanded) {} 193 194 /** 195 * Implement this to know when a direct reply is sent from a notification. 196 * @param key the notification key 197 */ onNotificationDirectReplied(@onNull String key)198 public void onNotificationDirectReplied(@NonNull String key) {} 199 200 /** 201 * Implement this to know when a suggested reply is sent. 202 * @param key the notification key 203 * @param reply the reply that is just sent 204 * @param source the source that provided the reply, e.g. SOURCE_FROM_APP 205 */ onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, @Source int source)206 public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply, 207 @Source int source) { 208 } 209 210 /** 211 * Implement this to know when an action is clicked. 212 * @param key the notification key 213 * @param action the action that is just clicked 214 * @param source the source that provided the action, e.g. SOURCE_FROM_APP 215 */ onActionInvoked(@onNull String key, @NonNull Notification.Action action, @Source int source)216 public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action, 217 @Source int source) { 218 } 219 220 /** 221 * Implement this to know when a user has changed which features of 222 * their notifications the assistant can modify. 223 * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what 224 * {@link Adjustment adjustments} you are currently allowed to make.</p> 225 */ onAllowedAdjustmentsChanged()226 public void onAllowedAdjustmentsChanged() { 227 } 228 229 /** 230 * Updates a notification. N.B. this won’t cause 231 * an existing notification to alert, but might allow a future update to 232 * this notification to alert. 233 * 234 * @param adjustment the adjustment with an explanation 235 */ adjustNotification(@onNull Adjustment adjustment)236 public final void adjustNotification(@NonNull Adjustment adjustment) { 237 if (!isBound()) return; 238 try { 239 setAdjustmentIssuer(adjustment); 240 getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment); 241 } catch (android.os.RemoteException ex) { 242 Log.v(TAG, "Unable to contact notification manager", ex); 243 throw ex.rethrowFromSystemServer(); 244 } 245 } 246 247 /** 248 * Updates existing notifications. Re-ranking won't occur until all adjustments are applied. 249 * N.B. this won’t cause an existing notification to alert, but might allow a future update to 250 * these notifications to alert. 251 * 252 * @param adjustments a list of adjustments with explanations 253 */ adjustNotifications(@onNull List<Adjustment> adjustments)254 public final void adjustNotifications(@NonNull List<Adjustment> adjustments) { 255 if (!isBound()) return; 256 try { 257 for (Adjustment adjustment : adjustments) { 258 setAdjustmentIssuer(adjustment); 259 } 260 getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments); 261 } catch (android.os.RemoteException ex) { 262 Log.v(TAG, "Unable to contact notification manager", ex); 263 throw ex.rethrowFromSystemServer(); 264 } 265 } 266 267 /** 268 * Inform the notification manager about un-snoozing a specific notification. 269 * <p> 270 * This should only be used for notifications snoozed because of a contextual snooze suggestion 271 * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a 272 * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the 273 * notification. 274 * @param key The key of the notification to snooze 275 */ unsnoozeNotification(@onNull String key)276 public final void unsnoozeNotification(@NonNull String key) { 277 if (!isBound()) return; 278 try { 279 getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key); 280 } catch (android.os.RemoteException ex) { 281 Log.v(TAG, "Unable to contact notification manager", ex); 282 } 283 } 284 285 private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper { 286 @Override onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, NotificationChannel channel)287 public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, 288 NotificationChannel channel) { 289 StatusBarNotification sbn; 290 try { 291 sbn = sbnHolder.get(); 292 } catch (RemoteException e) { 293 Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e); 294 return; 295 } 296 297 SomeArgs args = SomeArgs.obtain(); 298 args.arg1 = sbn; 299 args.arg2 = channel; 300 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED, 301 args).sendToTarget(); 302 } 303 304 @Override onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId)305 public void onNotificationSnoozedUntilContext( 306 IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) { 307 StatusBarNotification sbn; 308 try { 309 sbn = sbnHolder.get(); 310 } catch (RemoteException e) { 311 Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e); 312 return; 313 } 314 315 SomeArgs args = SomeArgs.obtain(); 316 args.arg1 = sbn; 317 args.arg2 = snoozeCriterionId; 318 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED, 319 args).sendToTarget(); 320 } 321 322 @Override onNotificationsSeen(List<String> keys)323 public void onNotificationsSeen(List<String> keys) { 324 SomeArgs args = SomeArgs.obtain(); 325 args.arg1 = keys; 326 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN, 327 args).sendToTarget(); 328 } 329 330 @Override onNotificationExpansionChanged(String key, boolean isUserAction, boolean isExpanded)331 public void onNotificationExpansionChanged(String key, boolean isUserAction, 332 boolean isExpanded) { 333 SomeArgs args = SomeArgs.obtain(); 334 args.arg1 = key; 335 args.argi1 = isUserAction ? 1 : 0; 336 args.argi2 = isExpanded ? 1 : 0; 337 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args) 338 .sendToTarget(); 339 } 340 341 @Override onNotificationDirectReply(String key)342 public void onNotificationDirectReply(String key) { 343 SomeArgs args = SomeArgs.obtain(); 344 args.arg1 = key; 345 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args) 346 .sendToTarget(); 347 } 348 349 @Override onSuggestedReplySent(String key, CharSequence reply, int source)350 public void onSuggestedReplySent(String key, CharSequence reply, int source) { 351 SomeArgs args = SomeArgs.obtain(); 352 args.arg1 = key; 353 args.arg2 = reply; 354 args.argi2 = source; 355 mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget(); 356 } 357 358 @Override onActionClicked(String key, Notification.Action action, int source)359 public void onActionClicked(String key, Notification.Action action, int source) { 360 SomeArgs args = SomeArgs.obtain(); 361 args.arg1 = key; 362 args.arg2 = action; 363 args.argi2 = source; 364 mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget(); 365 } 366 367 @Override onAllowedAdjustmentsChanged()368 public void onAllowedAdjustmentsChanged() { 369 mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget(); 370 } 371 } 372 setAdjustmentIssuer(@ullable Adjustment adjustment)373 private void setAdjustmentIssuer(@Nullable Adjustment adjustment) { 374 if (adjustment != null) { 375 adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName()); 376 } 377 } 378 379 private final class MyHandler extends Handler { 380 public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1; 381 public static final int MSG_ON_NOTIFICATION_SNOOZED = 2; 382 public static final int MSG_ON_NOTIFICATIONS_SEEN = 3; 383 public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4; 384 public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5; 385 public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6; 386 public static final int MSG_ON_ACTION_INVOKED = 7; 387 public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8; 388 MyHandler(Looper looper)389 public MyHandler(Looper looper) { 390 super(looper, null, false); 391 } 392 393 @Override handleMessage(Message msg)394 public void handleMessage(Message msg) { 395 switch (msg.what) { 396 case MSG_ON_NOTIFICATION_ENQUEUED: { 397 SomeArgs args = (SomeArgs) msg.obj; 398 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 399 NotificationChannel channel = (NotificationChannel) args.arg2; 400 args.recycle(); 401 Adjustment adjustment = onNotificationEnqueued(sbn, channel); 402 setAdjustmentIssuer(adjustment); 403 if (adjustment != null) { 404 if (!isBound()) { 405 Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip."); 406 return; 407 } 408 try { 409 getNotificationInterface().applyEnqueuedAdjustmentFromAssistant( 410 mWrapper, adjustment); 411 } catch (android.os.RemoteException ex) { 412 Log.v(TAG, "Unable to contact notification manager", ex); 413 throw ex.rethrowFromSystemServer(); 414 } catch (SecurityException e) { 415 // app cannot catch and recover from this, so do on their behalf 416 Log.w(TAG, "Enqueue adjustment failed; no longer connected", e); 417 } 418 } 419 break; 420 } 421 case MSG_ON_NOTIFICATION_SNOOZED: { 422 SomeArgs args = (SomeArgs) msg.obj; 423 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 424 String snoozeCriterionId = (String) args.arg2; 425 args.recycle(); 426 onNotificationSnoozedUntilContext(sbn, snoozeCriterionId); 427 break; 428 } 429 case MSG_ON_NOTIFICATIONS_SEEN: { 430 SomeArgs args = (SomeArgs) msg.obj; 431 List<String> keys = (List<String>) args.arg1; 432 args.recycle(); 433 onNotificationsSeen(keys); 434 break; 435 } 436 case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: { 437 SomeArgs args = (SomeArgs) msg.obj; 438 String key = (String) args.arg1; 439 boolean isUserAction = args.argi1 == 1; 440 boolean isExpanded = args.argi2 == 1; 441 args.recycle(); 442 onNotificationExpansionChanged(key, isUserAction, isExpanded); 443 break; 444 } 445 case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: { 446 SomeArgs args = (SomeArgs) msg.obj; 447 String key = (String) args.arg1; 448 args.recycle(); 449 onNotificationDirectReplied(key); 450 break; 451 } 452 case MSG_ON_SUGGESTED_REPLY_SENT: { 453 SomeArgs args = (SomeArgs) msg.obj; 454 String key = (String) args.arg1; 455 CharSequence reply = (CharSequence) args.arg2; 456 int source = args.argi2; 457 args.recycle(); 458 onSuggestedReplySent(key, reply, source); 459 break; 460 } 461 case MSG_ON_ACTION_INVOKED: { 462 SomeArgs args = (SomeArgs) msg.obj; 463 String key = (String) args.arg1; 464 Notification.Action action = (Notification.Action) args.arg2; 465 int source = args.argi2; 466 args.recycle(); 467 onActionInvoked(key, action, source); 468 break; 469 } 470 case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: { 471 onAllowedAdjustmentsChanged(); 472 break; 473 } 474 } 475 } 476 } 477 } 478