1 /* 2 * Copyright (C) 2013 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 android.annotation.CurrentTimeMillisLong; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SdkConstant; 24 import android.annotation.SystemApi; 25 import android.app.ActivityManager; 26 import android.app.INotificationManager; 27 import android.app.Notification; 28 import android.app.Notification.Builder; 29 import android.app.NotificationChannel; 30 import android.app.NotificationChannelGroup; 31 import android.app.NotificationManager; 32 import android.app.Person; 33 import android.app.Service; 34 import android.companion.CompanionDeviceManager; 35 import android.compat.annotation.UnsupportedAppUsage; 36 import android.content.ComponentName; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.pm.ParceledListSlice; 40 import android.content.pm.ShortcutInfo; 41 import android.graphics.Bitmap; 42 import android.graphics.drawable.BitmapDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.graphics.drawable.Icon; 45 import android.os.Build; 46 import android.os.Bundle; 47 import android.os.Handler; 48 import android.os.IBinder; 49 import android.os.Looper; 50 import android.os.Message; 51 import android.os.Parcel; 52 import android.os.Parcelable; 53 import android.os.RemoteException; 54 import android.os.ServiceManager; 55 import android.os.UserHandle; 56 import android.util.ArrayMap; 57 import android.util.Log; 58 import android.widget.RemoteViews; 59 60 import com.android.internal.annotations.GuardedBy; 61 import com.android.internal.annotations.VisibleForTesting; 62 import com.android.internal.os.SomeArgs; 63 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 import java.util.ArrayList; 67 import java.util.Collections; 68 import java.util.List; 69 import java.util.Objects; 70 71 /** 72 * A service that receives calls from the system when new notifications are 73 * posted or removed, or their ranking changed. 74 * <p>To extend this class, you must declare the service in your manifest file with 75 * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission 76 * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> 77 * <pre> 78 * <service android:name=".NotificationListener" 79 * android:label="@string/service_name" 80 * android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> 81 * <intent-filter> 82 * <action android:name="android.service.notification.NotificationListenerService" /> 83 * </intent-filter> 84 * <meta-data 85 * android:name="android.service.notification.default_filter_types" 86 * android:value="conversations|alerting"> 87 * </meta-data> 88 * <meta-data 89 * android:name="android.service.notification.disabled_filter_types" 90 * android:value="ongoing|silent"> 91 * </meta-data> 92 * </service></pre> 93 * 94 * <p>The service should wait for the {@link #onListenerConnected()} event 95 * before performing any operations. The {@link #requestRebind(ComponentName)} 96 * method is the <i>only</i> one that is safe to call before {@link #onListenerConnected()} 97 * or after {@link #onListenerDisconnected()}. 98 * </p> 99 * <p> Notification listeners cannot get notification access or be bound by the system on 100 * {@linkplain ActivityManager#isLowRamDevice() low-RAM} devices running Android Q (and below). 101 * The system also ignores notification listeners running in a work profile. A 102 * {@link android.app.admin.DevicePolicyManager} might block notifications originating from a work 103 * profile.</p> 104 * <p> 105 * From {@link Build.VERSION_CODES#N} onward all callbacks are called on the main thread. Prior 106 * to N, there is no guarantee on what thread the callback will happen. 107 * </p> 108 */ 109 public abstract class NotificationListenerService extends Service { 110 111 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 112 private final String TAG = getClass().getSimpleName(); 113 114 /** 115 * The name of the {@code meta-data} tag containing a pipe separated list of default 116 * integer notification types or "ongoing", "conversations", "alerting", or "silent" 117 * that should be provided to this listener. See 118 * {@link #FLAG_FILTER_TYPE_ONGOING}, 119 * {@link #FLAG_FILTER_TYPE_CONVERSATIONS}, {@link #FLAG_FILTER_TYPE_ALERTING), 120 * and {@link #FLAG_FILTER_TYPE_SILENT}. 121 * <p>This value will only be read if the app has not previously specified a default type list, 122 * and if the user has not overridden the allowed types.</p> 123 * <p>An absent value means 'allow all types'. 124 * A present but empty value means 'allow no types'.</p> 125 * 126 */ 127 public static final String META_DATA_DEFAULT_FILTER_TYPES 128 = "android.service.notification.default_filter_types"; 129 130 /** 131 * The name of the {@code meta-data} tag containing a comma separated list of default 132 * integer notification types that this listener never wants to receive. See 133 * {@link #FLAG_FILTER_TYPE_ONGOING}, 134 * {@link #FLAG_FILTER_TYPE_CONVERSATIONS}, {@link #FLAG_FILTER_TYPE_ALERTING), 135 * and {@link #FLAG_FILTER_TYPE_SILENT}. 136 * <p>Types provided in this list will appear as 'off' and 'disabled' in the user interface, 137 * so users don't enable a type that the listener will never bridge to their paired devices.</p> 138 * 139 */ 140 public static final String META_DATA_DISABLED_FILTER_TYPES 141 = "android.service.notification.disabled_filter_types"; 142 143 /** 144 * {@link #getCurrentInterruptionFilter() Interruption filter} constant - 145 * Normal interruption filter. 146 */ 147 public static final int INTERRUPTION_FILTER_ALL 148 = NotificationManager.INTERRUPTION_FILTER_ALL; 149 150 /** 151 * {@link #getCurrentInterruptionFilter() Interruption filter} constant - 152 * Priority interruption filter. 153 */ 154 public static final int INTERRUPTION_FILTER_PRIORITY 155 = NotificationManager.INTERRUPTION_FILTER_PRIORITY; 156 157 /** 158 * {@link #getCurrentInterruptionFilter() Interruption filter} constant - 159 * No interruptions filter. 160 */ 161 public static final int INTERRUPTION_FILTER_NONE 162 = NotificationManager.INTERRUPTION_FILTER_NONE; 163 164 /** 165 * {@link #getCurrentInterruptionFilter() Interruption filter} constant - 166 * Alarms only interruption filter. 167 */ 168 public static final int INTERRUPTION_FILTER_ALARMS 169 = NotificationManager.INTERRUPTION_FILTER_ALARMS; 170 171 /** {@link #getCurrentInterruptionFilter() Interruption filter} constant - returned when 172 * the value is unavailable for any reason. For example, before the notification listener 173 * is connected. 174 * 175 * {@see #onListenerConnected()} 176 */ 177 public static final int INTERRUPTION_FILTER_UNKNOWN 178 = NotificationManager.INTERRUPTION_FILTER_UNKNOWN; 179 180 /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI 181 * should disable notification sound, vibrating and other visual or aural effects. 182 * This does not change the interruption filter, only the effects. **/ 183 public static final int HINT_HOST_DISABLE_EFFECTS = 1; 184 185 /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI 186 * should disable notification sound, but not phone calls. 187 * This does not change the interruption filter, only the effects. **/ 188 public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 1 << 1; 189 190 /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI 191 * should disable phone call sounds, but not notification sound. 192 * This does not change the interruption filter, only the effects. **/ 193 public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 1 << 2; 194 195 /** 196 * Whether notification suppressed by DND should not interruption visually when the screen is 197 * off. 198 * 199 * @deprecated Use the more specific visual effects in {@link NotificationManager.Policy}. 200 */ 201 @Deprecated 202 public static final int SUPPRESSED_EFFECT_SCREEN_OFF = 203 NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; 204 /** 205 * Whether notification suppressed by DND should not interruption visually when the screen is 206 * on. 207 * 208 * @deprecated Use the more specific visual effects in {@link NotificationManager.Policy}. 209 */ 210 @Deprecated 211 public static final int SUPPRESSED_EFFECT_SCREEN_ON = 212 NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON; 213 214 215 // Notification cancellation reasons 216 217 /** Notification was canceled by the status bar reporting a notification click. */ 218 public static final int REASON_CLICK = 1; 219 /** Notification was canceled by the status bar reporting a user dismissal. */ 220 public static final int REASON_CANCEL = 2; 221 /** Notification was canceled by the status bar reporting a user dismiss all. */ 222 public static final int REASON_CANCEL_ALL = 3; 223 /** Notification was canceled by the status bar reporting an inflation error. */ 224 public static final int REASON_ERROR = 4; 225 /** Notification was canceled by the package manager modifying the package. */ 226 public static final int REASON_PACKAGE_CHANGED = 5; 227 /** Notification was canceled by the owning user context being stopped. */ 228 public static final int REASON_USER_STOPPED = 6; 229 /** Notification was canceled by the user banning the package. */ 230 public static final int REASON_PACKAGE_BANNED = 7; 231 /** Notification was canceled by the app canceling this specific notification. */ 232 public static final int REASON_APP_CANCEL = 8; 233 /** Notification was canceled by the app cancelling all its notifications. */ 234 public static final int REASON_APP_CANCEL_ALL = 9; 235 /** Notification was canceled by a listener reporting a user dismissal. */ 236 public static final int REASON_LISTENER_CANCEL = 10; 237 /** Notification was canceled by a listener reporting a user dismiss all. */ 238 public static final int REASON_LISTENER_CANCEL_ALL = 11; 239 /** Notification was canceled because it was a member of a canceled group. */ 240 public static final int REASON_GROUP_SUMMARY_CANCELED = 12; 241 /** Notification was canceled because it was an invisible member of a group. */ 242 public static final int REASON_GROUP_OPTIMIZATION = 13; 243 /** Notification was canceled by the device administrator suspending the package. */ 244 public static final int REASON_PACKAGE_SUSPENDED = 14; 245 /** Notification was canceled by the owning managed profile being turned off. */ 246 public static final int REASON_PROFILE_TURNED_OFF = 15; 247 /** Autobundled summary notification was canceled because its group was unbundled */ 248 public static final int REASON_UNAUTOBUNDLED = 16; 249 /** Notification was canceled by the user banning the channel. */ 250 public static final int REASON_CHANNEL_BANNED = 17; 251 /** Notification was snoozed. */ 252 public static final int REASON_SNOOZED = 18; 253 /** Notification was canceled due to timeout */ 254 public static final int REASON_TIMEOUT = 19; 255 /** Notification was canceled due to the backing channel being deleted */ 256 public static final int REASON_CHANNEL_REMOVED = 20; 257 /** Notification was canceled due to the app's storage being cleared */ 258 public static final int REASON_CLEAR_DATA = 21; 259 260 /** 261 * @hide 262 */ 263 @IntDef(prefix = "REASON_", value = { 264 REASON_CLICK, 265 REASON_CANCEL, 266 REASON_CANCEL_ALL, 267 REASON_ERROR, 268 REASON_PACKAGE_CHANGED, 269 REASON_USER_STOPPED, 270 REASON_PACKAGE_BANNED, 271 REASON_APP_CANCEL, 272 REASON_APP_CANCEL_ALL, 273 REASON_LISTENER_CANCEL, 274 REASON_LISTENER_CANCEL_ALL, 275 REASON_GROUP_SUMMARY_CANCELED, 276 REASON_GROUP_OPTIMIZATION, 277 REASON_PACKAGE_SUSPENDED, 278 REASON_PROFILE_TURNED_OFF, 279 REASON_UNAUTOBUNDLED, 280 REASON_CHANNEL_BANNED, 281 REASON_SNOOZED, 282 REASON_TIMEOUT 283 }) 284 public @interface NotificationCancelReason{}; 285 286 /** 287 * @hide 288 */ 289 @IntDef(flag = true, prefix = { "FLAG_FILTER_TYPE_" }, value = { 290 FLAG_FILTER_TYPE_CONVERSATIONS, 291 FLAG_FILTER_TYPE_ALERTING, 292 FLAG_FILTER_TYPE_SILENT, 293 FLAG_FILTER_TYPE_ONGOING 294 }) 295 public @interface NotificationFilterTypes {} 296 /** 297 * A flag value indicating that this notification listener can see conversation type 298 * notifications. 299 */ 300 public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; 301 /** 302 * A flag value indicating that this notification listener can see altering type notifications. 303 */ 304 public static final int FLAG_FILTER_TYPE_ALERTING = 2; 305 /** 306 * A flag value indicating that this notification listener can see silent type notifications. 307 */ 308 public static final int FLAG_FILTER_TYPE_SILENT = 4; 309 /** 310 * A flag value indicating that this notification listener can see important 311 * ( > {@link NotificationManager#IMPORTANCE_MIN}) ongoing type notifications. 312 */ 313 public static final int FLAG_FILTER_TYPE_ONGOING = 8; 314 315 /** 316 * The full trim of the StatusBarNotification including all its features. 317 * 318 * @hide 319 * @removed 320 */ 321 @SystemApi 322 public static final int TRIM_FULL = 0; 323 324 /** 325 * A light trim of the StatusBarNotification excluding the following features: 326 * 327 * <ol> 328 * <li>{@link Notification#tickerView tickerView}</li> 329 * <li>{@link Notification#contentView contentView}</li> 330 * <li>{@link Notification#largeIcon largeIcon}</li> 331 * <li>{@link Notification#bigContentView bigContentView}</li> 332 * <li>{@link Notification#headsUpContentView headsUpContentView}</li> 333 * <li>{@link Notification#EXTRA_LARGE_ICON extras[EXTRA_LARGE_ICON]}</li> 334 * <li>{@link Notification#EXTRA_LARGE_ICON_BIG extras[EXTRA_LARGE_ICON_BIG]}</li> 335 * <li>{@link Notification#EXTRA_PICTURE extras[EXTRA_PICTURE]}</li> 336 * <li>{@link Notification#EXTRA_BIG_TEXT extras[EXTRA_BIG_TEXT]}</li> 337 * </ol> 338 * 339 * @hide 340 * @removed 341 */ 342 @SystemApi 343 public static final int TRIM_LIGHT = 1; 344 345 346 /** @hide */ 347 @IntDef(prefix = { "NOTIFICATION_CHANNEL_OR_GROUP_" }, value = { 348 NOTIFICATION_CHANNEL_OR_GROUP_ADDED, 349 NOTIFICATION_CHANNEL_OR_GROUP_UPDATED, 350 NOTIFICATION_CHANNEL_OR_GROUP_DELETED 351 }) 352 @Retention(RetentionPolicy.SOURCE) 353 public @interface ChannelOrGroupModificationTypes {} 354 355 /** 356 * Channel or group modification reason provided to 357 * {@link #onNotificationChannelModified(String, UserHandle,NotificationChannel, int)} or 358 * {@link #onNotificationChannelGroupModified(String, UserHandle, NotificationChannelGroup, 359 * int)}- the provided object was created. 360 */ 361 public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1; 362 363 /** 364 * Channel or group modification reason provided to 365 * {@link #onNotificationChannelModified(String, UserHandle, NotificationChannel, int)} or 366 * {@link #onNotificationChannelGroupModified(String, UserHandle,NotificationChannelGroup, int)} 367 * - the provided object was updated. 368 */ 369 public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2; 370 371 /** 372 * Channel or group modification reason provided to 373 * {@link #onNotificationChannelModified(String, UserHandle, NotificationChannel, int)} or 374 * {@link #onNotificationChannelGroupModified(String, UserHandle, NotificationChannelGroup, 375 * int)}- the provided object was deleted. 376 */ 377 public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; 378 379 private final Object mLock = new Object(); 380 381 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 382 private Handler mHandler; 383 384 /** @hide */ 385 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 386 protected NotificationListenerWrapper mWrapper = null; 387 private boolean isConnected = false; 388 389 @GuardedBy("mLock") 390 private RankingMap mRankingMap; 391 392 /** 393 * @hide 394 */ 395 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 396 protected INotificationManager mNoMan; 397 398 /** 399 * Only valid after a successful call to (@link registerAsService}. 400 * @hide 401 */ 402 protected int mCurrentUser; 403 404 /** 405 * This context is required for system services since NotificationListenerService isn't 406 * started as a real Service and hence no context is available.. 407 * @hide 408 */ 409 protected Context mSystemContext; 410 411 /** 412 * The {@link Intent} that must be declared as handled by the service. 413 */ 414 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 415 public static final String SERVICE_INTERFACE 416 = "android.service.notification.NotificationListenerService"; 417 418 @Override attachBaseContext(Context base)419 protected void attachBaseContext(Context base) { 420 super.attachBaseContext(base); 421 mHandler = new MyHandler(getMainLooper()); 422 } 423 424 /** 425 * Implement this method to learn about new notifications as they are posted by apps. 426 * 427 * @param sbn A data structure encapsulating the original {@link android.app.Notification} 428 * object as well as its identifying information (tag and id) and source 429 * (package name). 430 */ onNotificationPosted(StatusBarNotification sbn)431 public void onNotificationPosted(StatusBarNotification sbn) { 432 // optional 433 } 434 435 /** 436 * Implement this method to learn about new notifications as they are posted by apps. 437 * 438 * @param sbn A data structure encapsulating the original {@link android.app.Notification} 439 * object as well as its identifying information (tag and id) and source 440 * (package name). 441 * @param rankingMap The current ranking map that can be used to retrieve ranking information 442 * for active notifications, including the newly posted one. 443 */ onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)444 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 445 onNotificationPosted(sbn); 446 } 447 448 /** 449 * Implement this method to learn when notifications are removed. 450 * <p> 451 * This might occur because the user has dismissed the notification using system UI (or another 452 * notification listener) or because the app has withdrawn the notification. 453 * <p> 454 * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the 455 * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight 456 * fields such as {@link android.app.Notification#contentView} and 457 * {@link android.app.Notification#largeIcon}. However, all other fields on 458 * {@link StatusBarNotification}, sufficient to match this call with a prior call to 459 * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. 460 * 461 * @param sbn A data structure encapsulating at least the original information (tag and id) 462 * and source (package name) used to post the {@link android.app.Notification} that 463 * was just removed. 464 */ onNotificationRemoved(StatusBarNotification sbn)465 public void onNotificationRemoved(StatusBarNotification sbn) { 466 // optional 467 } 468 469 /** 470 * Implement this method to learn when notifications are removed. 471 * <p> 472 * This might occur because the user has dismissed the notification using system UI (or another 473 * notification listener) or because the app has withdrawn the notification. 474 * <p> 475 * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the 476 * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight 477 * fields such as {@link android.app.Notification#contentView} and 478 * {@link android.app.Notification#largeIcon}. However, all other fields on 479 * {@link StatusBarNotification}, sufficient to match this call with a prior call to 480 * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. 481 * 482 * @param sbn A data structure encapsulating at least the original information (tag and id) 483 * and source (package name) used to post the {@link android.app.Notification} that 484 * was just removed. 485 * @param rankingMap The current ranking map that can be used to retrieve ranking information 486 * for active notifications. 487 * 488 */ onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)489 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 490 onNotificationRemoved(sbn); 491 } 492 493 494 /** 495 * Implement this method to learn when notifications are removed and why. 496 * <p> 497 * This might occur because the user has dismissed the notification using system UI (or another 498 * notification listener) or because the app has withdrawn the notification. 499 * <p> 500 * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the 501 * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight 502 * fields such as {@link android.app.Notification#contentView} and 503 * {@link android.app.Notification#largeIcon}. However, all other fields on 504 * {@link StatusBarNotification}, sufficient to match this call with a prior call to 505 * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. 506 * 507 ** @param sbn A data structure encapsulating at least the original information (tag and id) 508 * and source (package name) used to post the {@link android.app.Notification} that 509 * was just removed. 510 * @param rankingMap The current ranking map that can be used to retrieve ranking information 511 * for active notifications. 512 * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. 513 */ onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)514 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 515 int reason) { 516 onNotificationRemoved(sbn, rankingMap); 517 } 518 519 /** 520 * NotificationStats are not populated for notification listeners, so fall back to 521 * {@link #onNotificationRemoved(StatusBarNotification, RankingMap, int)}. 522 * 523 * @hide 524 */ 525 @SystemApi onNotificationRemoved(@onNull StatusBarNotification sbn, @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason)526 public void onNotificationRemoved(@NonNull StatusBarNotification sbn, 527 @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) { 528 onNotificationRemoved(sbn, rankingMap, reason); 529 } 530 531 /** 532 * Implement this method to learn about when the listener is enabled and connected to 533 * the notification manager. You are safe to call {@link #getActiveNotifications()} 534 * at this time. 535 */ onListenerConnected()536 public void onListenerConnected() { 537 // optional 538 } 539 540 /** 541 * Implement this method to learn about when the listener is disconnected from the 542 * notification manager.You will not receive any events after this call, and may only 543 * call {@link #requestRebind(ComponentName)} at this time. 544 */ onListenerDisconnected()545 public void onListenerDisconnected() { 546 // optional 547 } 548 549 /** 550 * Implement this method to be notified when the notification ranking changes. 551 * 552 * @param rankingMap The current ranking map that can be used to retrieve ranking information 553 * for active notifications. 554 */ onNotificationRankingUpdate(RankingMap rankingMap)555 public void onNotificationRankingUpdate(RankingMap rankingMap) { 556 // optional 557 } 558 559 /** 560 * Implement this method to be notified when the 561 * {@link #getCurrentListenerHints() Listener hints} change. 562 * 563 * @param hints The current {@link #getCurrentListenerHints() listener hints}. 564 */ onListenerHintsChanged(int hints)565 public void onListenerHintsChanged(int hints) { 566 // optional 567 } 568 569 /** 570 * Implement this method to be notified when the behavior of silent notifications in the status 571 * bar changes. See {@link NotificationManager#shouldHideSilentStatusBarIcons()}. 572 * 573 * @param hideSilentStatusIcons whether or not status bar icons should be hidden for silent 574 * notifications 575 */ onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons)576 public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) { 577 // optional 578 } 579 580 /** 581 * Implement this method to learn about notification channel modifications. 582 * 583 * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated 584 * device} in order to receive this callback. 585 * 586 * @param pkg The package the channel belongs to. 587 * @param user The user on which the change was made. 588 * @param channel The channel that has changed. 589 * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED}, 590 * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED}, 591 * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}. 592 */ onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType)593 public void onNotificationChannelModified(String pkg, UserHandle user, 594 NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType) { 595 // optional 596 } 597 598 /** 599 * Implement this method to learn about notification channel group modifications. 600 * 601 * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated 602 * device} in order to receive this callback. 603 * 604 * @param pkg The package the group belongs to. 605 * @param user The user on which the change was made. 606 * @param group The group that has changed. 607 * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED}, 608 * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED}, 609 * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}. 610 */ onNotificationChannelGroupModified(String pkg, UserHandle user, NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType)611 public void onNotificationChannelGroupModified(String pkg, UserHandle user, 612 NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType) { 613 // optional 614 } 615 616 /** 617 * Implement this method to be notified when the 618 * {@link #getCurrentInterruptionFilter() interruption filter} changed. 619 * 620 * @param interruptionFilter The current 621 * {@link #getCurrentInterruptionFilter() interruption filter}. 622 */ onInterruptionFilterChanged(int interruptionFilter)623 public void onInterruptionFilterChanged(int interruptionFilter) { 624 // optional 625 } 626 627 /** @hide */ 628 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) getNotificationInterface()629 protected final INotificationManager getNotificationInterface() { 630 if (mNoMan == null) { 631 mNoMan = INotificationManager.Stub.asInterface( 632 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 633 } 634 return mNoMan; 635 } 636 637 /** 638 * Inform the notification manager about dismissal of a single notification. 639 * <p> 640 * Use this if your listener has a user interface that allows the user to dismiss individual 641 * notifications, similar to the behavior of Android's status bar and notification panel. 642 * It should be called after the user dismisses a single notification using your UI; 643 * upon being informed, the notification manager will actually remove the notification 644 * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback. 645 * <p> 646 * <b>Note:</b> If your listener allows the user to fire a notification's 647 * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call 648 * this method at that time <i>if</i> the Notification in question has the 649 * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set. 650 * 651 * <p>The service should wait for the {@link #onListenerConnected()} event 652 * before performing this operation. 653 * 654 * @param pkg Package of the notifying app. 655 * @param tag Tag of the notification as specified by the notifying app in 656 * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}. 657 * @param id ID of the notification as specified by the notifying app in 658 * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}. 659 * <p> 660 * @deprecated Use {@link #cancelNotification(String key)} 661 * instead. Beginning with {@link android.os.Build.VERSION_CODES#LOLLIPOP} this method will no longer 662 * cancel the notification. It will continue to cancel the notification for applications 663 * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP}. 664 */ 665 @Deprecated cancelNotification(String pkg, String tag, int id)666 public final void cancelNotification(String pkg, String tag, int id) { 667 if (!isBound()) return; 668 try { 669 getNotificationInterface().cancelNotificationFromListener( 670 mWrapper, pkg, tag, id); 671 } catch (android.os.RemoteException ex) { 672 Log.v(TAG, "Unable to contact notification manager", ex); 673 } 674 } 675 676 /** 677 * Inform the notification manager about dismissal of a single notification. 678 * <p> 679 * Use this if your listener has a user interface that allows the user to dismiss individual 680 * notifications, similar to the behavior of Android's status bar and notification panel. 681 * It should be called after the user dismisses a single notification using your UI; 682 * upon being informed, the notification manager will actually remove the notification 683 * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback. 684 * <p> 685 * <b>Note:</b> If your listener allows the user to fire a notification's 686 * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call 687 * this method at that time <i>if</i> the Notification in question has the 688 * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set. 689 * <p> 690 * 691 * <p>The service should wait for the {@link #onListenerConnected()} event 692 * before performing this operation. 693 * 694 * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}. 695 */ cancelNotification(String key)696 public final void cancelNotification(String key) { 697 if (!isBound()) return; 698 try { 699 getNotificationInterface().cancelNotificationsFromListener(mWrapper, 700 new String[] { key }); 701 } catch (android.os.RemoteException ex) { 702 Log.v(TAG, "Unable to contact notification manager", ex); 703 } 704 } 705 706 /** 707 * Inform the notification manager about dismissal of all notifications. 708 * <p> 709 * Use this if your listener has a user interface that allows the user to dismiss all 710 * notifications, similar to the behavior of Android's status bar and notification panel. 711 * It should be called after the user invokes the "dismiss all" function of your UI; 712 * upon being informed, the notification manager will actually remove all active notifications 713 * and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks. 714 * 715 * <p>The service should wait for the {@link #onListenerConnected()} event 716 * before performing this operation. 717 * 718 * {@see #cancelNotification(String, String, int)} 719 */ cancelAllNotifications()720 public final void cancelAllNotifications() { 721 cancelNotifications(null /*all*/); 722 } 723 724 /** 725 * Inform the notification manager about dismissal of specific notifications. 726 * <p> 727 * Use this if your listener has a user interface that allows the user to dismiss 728 * multiple notifications at once. 729 * 730 * <p>The service should wait for the {@link #onListenerConnected()} event 731 * before performing this operation. 732 * 733 * @param keys Notifications to dismiss, or {@code null} to dismiss all. 734 * 735 * {@see #cancelNotification(String, String, int)} 736 */ cancelNotifications(String[] keys)737 public final void cancelNotifications(String[] keys) { 738 if (!isBound()) return; 739 try { 740 getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys); 741 } catch (android.os.RemoteException ex) { 742 Log.v(TAG, "Unable to contact notification manager", ex); 743 } 744 } 745 746 /** 747 * Inform the notification manager about snoozing a specific notification. 748 * <p> 749 * Use this if your listener has a user interface that allows the user to snooze a notification 750 * until a given {@link SnoozeCriterion}. It should be called after the user snoozes a single 751 * notification using your UI; upon being informed, the notification manager will actually 752 * remove the notification and you will get an 753 * {@link #onNotificationRemoved(StatusBarNotification)} callback. When the snoozing period 754 * expires, you will get a {@link #onNotificationPosted(StatusBarNotification, RankingMap)} 755 * callback for the notification. 756 * @param key The key of the notification to snooze 757 * @param snoozeCriterionId The{@link SnoozeCriterion#getId()} of a context to snooze the 758 * notification until. 759 * @hide 760 * @removed 761 */ 762 @SystemApi snoozeNotification(String key, String snoozeCriterionId)763 public final void snoozeNotification(String key, String snoozeCriterionId) { 764 if (!isBound()) return; 765 try { 766 getNotificationInterface().snoozeNotificationUntilContextFromListener( 767 mWrapper, key, snoozeCriterionId); 768 } catch (android.os.RemoteException ex) { 769 Log.v(TAG, "Unable to contact notification manager", ex); 770 } 771 } 772 773 /** 774 * Inform the notification manager about snoozing a specific notification. 775 * <p> 776 * Use this if your listener has a user interface that allows the user to snooze a notification 777 * for a time. It should be called after the user snoozes a single notification using 778 * your UI; upon being informed, the notification manager will actually remove the notification 779 * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback. When the 780 * snoozing period expires, you will get a 781 * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the 782 * notification. 783 * @param key The key of the notification to snooze 784 * @param durationMs A duration to snooze the notification for, in milliseconds. 785 */ snoozeNotification(String key, long durationMs)786 public final void snoozeNotification(String key, long durationMs) { 787 if (!isBound()) return; 788 try { 789 getNotificationInterface().snoozeNotificationUntilFromListener( 790 mWrapper, key, durationMs); 791 } catch (android.os.RemoteException ex) { 792 Log.v(TAG, "Unable to contact notification manager", ex); 793 } 794 } 795 796 /** 797 * Lets an app migrate notification filters from its app into the OS. 798 * 799 * <p>This call will be ignored if the app has already migrated these settings or the user 800 * has set filters in the UI. This method is intended for user specific settings; if an app has 801 * already specified defaults types in its manifest with 802 * {@link #META_DATA_DEFAULT_FILTER_TYPES}, the defaultTypes option will be ignored.</p> 803 * @param defaultTypes A value representing the types of notifications that this listener should 804 * receive by default 805 * @param disallowedPkgs A list of package names whose notifications should not be seen by this 806 * listener, by default, because the listener does not process or display them, or because a 807 * user had previously disallowed these packages in the listener app's UI 808 */ migrateNotificationFilter(@otificationFilterTypes int defaultTypes, @Nullable List<String> disallowedPkgs)809 public final void migrateNotificationFilter(@NotificationFilterTypes int defaultTypes, 810 @Nullable List<String> disallowedPkgs) { 811 if (!isBound()) return; 812 try { 813 getNotificationInterface().migrateNotificationFilter( 814 mWrapper, defaultTypes, disallowedPkgs); 815 } catch (android.os.RemoteException ex) { 816 Log.v(TAG, "Unable to contact notification manager", ex); 817 } 818 } 819 820 /** 821 * Inform the notification manager that these notifications have been viewed by the 822 * user. This should only be called when there is sufficient confidence that the user is 823 * looking at the notifications, such as when the notifications appear on the screen due to 824 * an explicit user interaction. 825 * 826 * <p>The service should wait for the {@link #onListenerConnected()} event 827 * before performing this operation. 828 * 829 * @param keys Notifications to mark as seen. 830 */ setNotificationsShown(String[] keys)831 public final void setNotificationsShown(String[] keys) { 832 if (!isBound()) return; 833 try { 834 getNotificationInterface().setNotificationsShownFromListener(mWrapper, keys); 835 } catch (android.os.RemoteException ex) { 836 Log.v(TAG, "Unable to contact notification manager", ex); 837 } 838 } 839 840 841 /** 842 * Updates a notification channel for a given package for a given user. This should only be used 843 * to reflect changes a user has made to the channel via the listener's user interface. 844 * 845 * <p>This method will throw a security exception if you don't have access to notifications 846 * for the given user.</p> 847 * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated 848 * device} in order to use this method. 849 * 850 * @param pkg The package the channel belongs to. 851 * @param user The user the channel belongs to. 852 * @param channel the channel to update. 853 */ updateNotificationChannel(@onNull String pkg, @NonNull UserHandle user, @NonNull NotificationChannel channel)854 public final void updateNotificationChannel(@NonNull String pkg, @NonNull UserHandle user, 855 @NonNull NotificationChannel channel) { 856 if (!isBound()) return; 857 try { 858 getNotificationInterface().updateNotificationChannelFromPrivilegedListener( 859 mWrapper, pkg, user, channel); 860 } catch (RemoteException e) { 861 Log.v(TAG, "Unable to contact notification manager", e); 862 throw e.rethrowFromSystemServer(); 863 } 864 } 865 866 /** 867 * Returns all notification channels belonging to the given package for a given user. 868 * 869 * <p>This method will throw a security exception if you don't have access to notifications 870 * for the given user.</p> 871 * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated 872 * device} or be the {@link NotificationAssistantService notification assistant} in order to 873 * use this method. 874 * 875 * @param pkg The package to retrieve channels for. 876 */ getNotificationChannels(@onNull String pkg, @NonNull UserHandle user)877 public final List<NotificationChannel> getNotificationChannels(@NonNull String pkg, 878 @NonNull UserHandle user) { 879 if (!isBound()) return null; 880 try { 881 882 return getNotificationInterface().getNotificationChannelsFromPrivilegedListener( 883 mWrapper, pkg, user).getList(); 884 } catch (RemoteException e) { 885 Log.v(TAG, "Unable to contact notification manager", e); 886 throw e.rethrowFromSystemServer(); 887 } 888 } 889 890 /** 891 * Returns all notification channel groups belonging to the given package for a given user. 892 * 893 * <p>This method will throw a security exception if you don't have access to notifications 894 * for the given user.</p> 895 * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated 896 * device} or be the {@link NotificationAssistantService notification assistant} in order to 897 * use this method. 898 * 899 * @param pkg The package to retrieve channel groups for. 900 */ getNotificationChannelGroups(@onNull String pkg, @NonNull UserHandle user)901 public final List<NotificationChannelGroup> getNotificationChannelGroups(@NonNull String pkg, 902 @NonNull UserHandle user) { 903 if (!isBound()) return null; 904 try { 905 906 return getNotificationInterface().getNotificationChannelGroupsFromPrivilegedListener( 907 mWrapper, pkg, user).getList(); 908 } catch (RemoteException e) { 909 Log.v(TAG, "Unable to contact notification manager", e); 910 throw e.rethrowFromSystemServer(); 911 } 912 } 913 914 /** 915 * Sets the notification trim that will be received via {@link #onNotificationPosted}. 916 * 917 * <p> 918 * Setting a trim other than {@link #TRIM_FULL} enables listeners that don't need access to the 919 * full notification features right away to reduce their memory footprint. Full notifications 920 * can be requested on-demand via {@link #getActiveNotifications(int)}. 921 * 922 * <p> 923 * Set to {@link #TRIM_FULL} initially. 924 * 925 * <p>The service should wait for the {@link #onListenerConnected()} event 926 * before performing this operation. 927 * 928 * @hide 929 * @removed 930 * 931 * @param trim trim of the notifications to be passed via {@link #onNotificationPosted}. 932 * See <code>TRIM_*</code> constants. 933 */ 934 @SystemApi setOnNotificationPostedTrim(int trim)935 public final void setOnNotificationPostedTrim(int trim) { 936 if (!isBound()) return; 937 try { 938 getNotificationInterface().setOnNotificationPostedTrimFromListener(mWrapper, trim); 939 } catch (RemoteException ex) { 940 Log.v(TAG, "Unable to contact notification manager", ex); 941 } 942 } 943 944 /** 945 * Request the list of outstanding notifications (that is, those that are visible to the 946 * current user). Useful when you don't know what's already been posted. 947 * 948 * <p>The service should wait for the {@link #onListenerConnected()} event 949 * before performing this operation. 950 * 951 * @return An array of active notifications, sorted in natural order. 952 */ getActiveNotifications()953 public StatusBarNotification[] getActiveNotifications() { 954 StatusBarNotification[] activeNotifications = getActiveNotifications(null, TRIM_FULL); 955 return activeNotifications != null ? activeNotifications : new StatusBarNotification[0]; 956 } 957 958 /** 959 * Like {@link #getActiveNotifications()}, but returns the list of currently snoozed 960 * notifications, for all users this listener has access to. 961 * 962 * <p>The service should wait for the {@link #onListenerConnected()} event 963 * before performing this operation. 964 * 965 * @return An array of snoozed notifications, sorted in natural order. 966 */ getSnoozedNotifications()967 public final StatusBarNotification[] getSnoozedNotifications() { 968 try { 969 ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface() 970 .getSnoozedNotificationsFromListener(mWrapper, TRIM_FULL); 971 return cleanUpNotificationList(parceledList); 972 } catch (android.os.RemoteException ex) { 973 Log.v(TAG, "Unable to contact notification manager", ex); 974 } 975 return null; 976 } 977 978 /** 979 * Request the list of outstanding notifications (that is, those that are visible to the 980 * current user). Useful when you don't know what's already been posted. 981 * 982 * @hide 983 * @removed 984 * 985 * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants. 986 * @return An array of active notifications, sorted in natural order. 987 */ 988 @SystemApi getActiveNotifications(int trim)989 public StatusBarNotification[] getActiveNotifications(int trim) { 990 StatusBarNotification[] activeNotifications = getActiveNotifications(null, trim); 991 return activeNotifications != null ? activeNotifications : new StatusBarNotification[0]; 992 } 993 994 /** 995 * Request one or more notifications by key. Useful if you have been keeping track of 996 * notifications but didn't want to retain the bits, and now need to go back and extract 997 * more data out of those notifications. 998 * 999 * <p>The service should wait for the {@link #onListenerConnected()} event 1000 * before performing this operation. 1001 * 1002 * @param keys the keys of the notifications to request 1003 * @return An array of notifications corresponding to the requested keys, in the 1004 * same order as the key list. 1005 */ getActiveNotifications(String[] keys)1006 public StatusBarNotification[] getActiveNotifications(String[] keys) { 1007 StatusBarNotification[] activeNotifications = getActiveNotifications(keys, TRIM_FULL); 1008 return activeNotifications != null ? activeNotifications : new StatusBarNotification[0]; 1009 } 1010 1011 /** 1012 * Request one or more notifications by key. Useful if you have been keeping track of 1013 * notifications but didn't want to retain the bits, and now need to go back and extract 1014 * more data out of those notifications. 1015 * 1016 * @hide 1017 * @removed 1018 * 1019 * @param keys the keys of the notifications to request 1020 * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants. 1021 * @return An array of notifications corresponding to the requested keys, in the 1022 * same order as the key list. 1023 */ 1024 @SystemApi getActiveNotifications(String[] keys, int trim)1025 public StatusBarNotification[] getActiveNotifications(String[] keys, int trim) { 1026 if (!isBound()) 1027 return null; 1028 try { 1029 ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface() 1030 .getActiveNotificationsFromListener(mWrapper, keys, trim); 1031 return cleanUpNotificationList(parceledList); 1032 } catch (android.os.RemoteException ex) { 1033 Log.v(TAG, "Unable to contact notification manager", ex); 1034 } 1035 return null; 1036 } 1037 cleanUpNotificationList( ParceledListSlice<StatusBarNotification> parceledList)1038 private StatusBarNotification[] cleanUpNotificationList( 1039 ParceledListSlice<StatusBarNotification> parceledList) { 1040 if (parceledList == null || parceledList.getList() == null) { 1041 return new StatusBarNotification[0]; 1042 } 1043 List<StatusBarNotification> list = parceledList.getList(); 1044 ArrayList<StatusBarNotification> corruptNotifications = null; 1045 int N = list.size(); 1046 for (int i = 0; i < N; i++) { 1047 StatusBarNotification sbn = list.get(i); 1048 Notification notification = sbn.getNotification(); 1049 try { 1050 // convert icon metadata to legacy format for older clients 1051 createLegacyIconExtras(notification); 1052 // populate remote views for older clients. 1053 maybePopulateRemoteViews(notification); 1054 // populate people for older clients. 1055 maybePopulatePeople(notification); 1056 } catch (IllegalArgumentException e) { 1057 if (corruptNotifications == null) { 1058 corruptNotifications = new ArrayList<>(N); 1059 } 1060 corruptNotifications.add(sbn); 1061 Log.w(TAG, "get(Active/Snoozed)Notifications: can't rebuild notification from " + 1062 sbn.getPackageName()); 1063 } 1064 } 1065 if (corruptNotifications != null) { 1066 list.removeAll(corruptNotifications); 1067 } 1068 return list.toArray(new StatusBarNotification[list.size()]); 1069 } 1070 1071 /** 1072 * Gets the set of hints representing current state. 1073 * 1074 * <p> 1075 * The current state may differ from the requested state if the hint represents state 1076 * shared across all listeners or a feature the notification host does not support or refuses 1077 * to grant. 1078 * 1079 * <p>The service should wait for the {@link #onListenerConnected()} event 1080 * before performing this operation. 1081 * 1082 * @return Zero or more of the HINT_ constants. 1083 */ getCurrentListenerHints()1084 public final int getCurrentListenerHints() { 1085 if (!isBound()) return 0; 1086 try { 1087 return getNotificationInterface().getHintsFromListener(mWrapper); 1088 } catch (android.os.RemoteException ex) { 1089 Log.v(TAG, "Unable to contact notification manager", ex); 1090 return 0; 1091 } 1092 } 1093 1094 /** 1095 * Gets the current notification interruption filter active on the host. 1096 * 1097 * <p> 1098 * The interruption filter defines which notifications are allowed to interrupt the user 1099 * (e.g. via sound & vibration) and is applied globally. Listeners can find out whether 1100 * a specific notification matched the interruption filter via 1101 * {@link Ranking#matchesInterruptionFilter()}. 1102 * <p> 1103 * The current filter may differ from the previously requested filter if the notification host 1104 * does not support or refuses to apply the requested filter, or if another component changed 1105 * the filter in the meantime. 1106 * <p> 1107 * Listen for updates using {@link #onInterruptionFilterChanged(int)}. 1108 * 1109 * <p>The service should wait for the {@link #onListenerConnected()} event 1110 * before performing this operation. 1111 * 1112 * @return One of the INTERRUPTION_FILTER_ constants, or INTERRUPTION_FILTER_UNKNOWN when 1113 * unavailable. 1114 */ getCurrentInterruptionFilter()1115 public final int getCurrentInterruptionFilter() { 1116 if (!isBound()) return INTERRUPTION_FILTER_UNKNOWN; 1117 try { 1118 return getNotificationInterface().getInterruptionFilterFromListener(mWrapper); 1119 } catch (android.os.RemoteException ex) { 1120 Log.v(TAG, "Unable to contact notification manager", ex); 1121 return INTERRUPTION_FILTER_UNKNOWN; 1122 } 1123 } 1124 1125 /** 1126 * Clears listener hints set via {@link #getCurrentListenerHints()}. 1127 * 1128 * <p>The service should wait for the {@link #onListenerConnected()} event 1129 * before performing this operation. 1130 */ clearRequestedListenerHints()1131 public final void clearRequestedListenerHints() { 1132 if (!isBound()) return; 1133 try { 1134 getNotificationInterface().clearRequestedListenerHints(mWrapper); 1135 } catch (android.os.RemoteException ex) { 1136 Log.v(TAG, "Unable to contact notification manager", ex); 1137 } 1138 } 1139 1140 /** 1141 * Sets the desired {@link #getCurrentListenerHints() listener hints}. 1142 * 1143 * <p> 1144 * This is merely a request, the host may or may not choose to take action depending 1145 * on other listener requests or other global state. 1146 * <p> 1147 * Listen for updates using {@link #onListenerHintsChanged(int)}. 1148 * 1149 * <p>The service should wait for the {@link #onListenerConnected()} event 1150 * before performing this operation. 1151 * 1152 * @param hints One or more of the HINT_ constants. 1153 */ requestListenerHints(int hints)1154 public final void requestListenerHints(int hints) { 1155 if (!isBound()) return; 1156 try { 1157 getNotificationInterface().requestHintsFromListener(mWrapper, hints); 1158 } catch (android.os.RemoteException ex) { 1159 Log.v(TAG, "Unable to contact notification manager", ex); 1160 } 1161 } 1162 1163 /** 1164 * Sets the desired {@link #getCurrentInterruptionFilter() interruption filter}. 1165 * 1166 * <p> 1167 * This is merely a request, the host may or may not choose to apply the requested 1168 * interruption filter depending on other listener requests or other global state. 1169 * <p> 1170 * Listen for updates using {@link #onInterruptionFilterChanged(int)}. 1171 * 1172 * <p>The service should wait for the {@link #onListenerConnected()} event 1173 * before performing this operation. 1174 * 1175 * @param interruptionFilter One of the INTERRUPTION_FILTER_ constants. 1176 */ requestInterruptionFilter(int interruptionFilter)1177 public final void requestInterruptionFilter(int interruptionFilter) { 1178 if (!isBound()) return; 1179 try { 1180 getNotificationInterface() 1181 .requestInterruptionFilterFromListener(mWrapper, interruptionFilter); 1182 } catch (android.os.RemoteException ex) { 1183 Log.v(TAG, "Unable to contact notification manager", ex); 1184 } 1185 } 1186 1187 /** 1188 * Returns current ranking information. 1189 * 1190 * <p> 1191 * The returned object represents the current ranking snapshot and only 1192 * applies for currently active notifications. 1193 * <p> 1194 * Generally you should use the RankingMap that is passed with events such 1195 * as {@link #onNotificationPosted(StatusBarNotification, RankingMap)}, 1196 * {@link #onNotificationRemoved(StatusBarNotification, RankingMap)}, and 1197 * so on. This method should only be used when needing access outside of 1198 * such events, for example to retrieve the RankingMap right after 1199 * initialization. 1200 * 1201 * <p>The service should wait for the {@link #onListenerConnected()} event 1202 * before performing this operation. 1203 * 1204 * @return A {@link RankingMap} object providing access to ranking information 1205 */ getCurrentRanking()1206 public RankingMap getCurrentRanking() { 1207 synchronized (mLock) { 1208 return mRankingMap; 1209 } 1210 } 1211 1212 /** 1213 * This is not the lifecycle event you are looking for. 1214 * 1215 * <p>The service should wait for the {@link #onListenerConnected()} event 1216 * before performing any operations. 1217 */ 1218 @Override onBind(Intent intent)1219 public IBinder onBind(Intent intent) { 1220 if (mWrapper == null) { 1221 mWrapper = new NotificationListenerWrapper(); 1222 } 1223 return mWrapper; 1224 } 1225 1226 /** @hide */ 1227 @UnsupportedAppUsage isBound()1228 protected boolean isBound() { 1229 if (mWrapper == null) { 1230 Log.w(TAG, "Notification listener service not yet bound."); 1231 return false; 1232 } 1233 return true; 1234 } 1235 1236 @Override onDestroy()1237 public void onDestroy() { 1238 onListenerDisconnected(); 1239 super.onDestroy(); 1240 } 1241 1242 /** 1243 * Directly register this service with the Notification Manager. 1244 * 1245 * <p>Only system services may use this call. It will fail for non-system callers. 1246 * Apps should ask the user to add their listener in Settings. 1247 * 1248 * @param context Context required for accessing resources. Since this service isn't 1249 * launched as a real Service when using this method, a context has to be passed in. 1250 * @param componentName the component that will consume the notification information 1251 * @param currentUser the user to use as the stream filter 1252 * @hide 1253 * @removed 1254 */ 1255 @SystemApi registerAsSystemService(Context context, ComponentName componentName, int currentUser)1256 public void registerAsSystemService(Context context, ComponentName componentName, 1257 int currentUser) throws RemoteException { 1258 if (mWrapper == null) { 1259 mWrapper = new NotificationListenerWrapper(); 1260 } 1261 mSystemContext = context; 1262 INotificationManager noMan = getNotificationInterface(); 1263 mHandler = new MyHandler(context.getMainLooper()); 1264 mCurrentUser = currentUser; 1265 noMan.registerListener(mWrapper, componentName, currentUser); 1266 } 1267 1268 /** 1269 * Directly unregister this service from the Notification Manager. 1270 * 1271 * <p>This method will fail for listeners that were not registered 1272 * with (@link registerAsService). 1273 * @hide 1274 * @removed 1275 */ 1276 @SystemApi unregisterAsSystemService()1277 public void unregisterAsSystemService() throws RemoteException { 1278 if (mWrapper != null) { 1279 INotificationManager noMan = getNotificationInterface(); 1280 noMan.unregisterListener(mWrapper, mCurrentUser); 1281 } 1282 } 1283 1284 /** 1285 * Request that the listener be rebound, after a previous call to {@link #requestUnbind}. 1286 * 1287 * <p>This method will fail for listeners that have 1288 * not been granted the permission by the user. 1289 */ requestRebind(ComponentName componentName)1290 public static void requestRebind(ComponentName componentName) { 1291 INotificationManager noMan = INotificationManager.Stub.asInterface( 1292 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 1293 try { 1294 noMan.requestBindListener(componentName); 1295 } catch (RemoteException ex) { 1296 throw ex.rethrowFromSystemServer(); 1297 } 1298 } 1299 1300 /** 1301 * Request that the service be unbound. 1302 * 1303 * <p>Once this is called, you will no longer receive updates and no method calls are 1304 * guaranteed to be successful, until you next receive the {@link #onListenerConnected()} event. 1305 * The service will likely be killed by the system after this call. 1306 * 1307 * <p>The service should wait for the {@link #onListenerConnected()} event 1308 * before performing this operation. I know it's tempting, but you must wait. 1309 */ requestUnbind()1310 public final void requestUnbind() { 1311 if (mWrapper != null) { 1312 INotificationManager noMan = getNotificationInterface(); 1313 try { 1314 noMan.requestUnbindListener(mWrapper); 1315 // Disable future messages. 1316 isConnected = false; 1317 } catch (RemoteException ex) { 1318 throw ex.rethrowFromSystemServer(); 1319 } 1320 } 1321 } 1322 1323 /** 1324 * Convert new-style Icons to legacy representations for pre-M clients. 1325 * @hide 1326 */ createLegacyIconExtras(Notification n)1327 public final void createLegacyIconExtras(Notification n) { 1328 if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M) { 1329 Icon smallIcon = n.getSmallIcon(); 1330 Icon largeIcon = n.getLargeIcon(); 1331 if (smallIcon != null && smallIcon.getType() == Icon.TYPE_RESOURCE) { 1332 n.extras.putInt(Notification.EXTRA_SMALL_ICON, smallIcon.getResId()); 1333 n.icon = smallIcon.getResId(); 1334 } 1335 if (largeIcon != null) { 1336 Drawable d = largeIcon.loadDrawable(getContext()); 1337 if (d != null && d instanceof BitmapDrawable) { 1338 final Bitmap largeIconBits = ((BitmapDrawable) d).getBitmap(); 1339 n.extras.putParcelable(Notification.EXTRA_LARGE_ICON, largeIconBits); 1340 n.largeIcon = largeIconBits; 1341 } 1342 } 1343 } 1344 } 1345 1346 /** 1347 * Populates remote views for pre-N targeting apps. 1348 */ maybePopulateRemoteViews(Notification notification)1349 private void maybePopulateRemoteViews(Notification notification) { 1350 if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { 1351 Builder builder = Builder.recoverBuilder(getContext(), notification); 1352 1353 // Some styles wrap Notification's contentView, bigContentView and headsUpContentView. 1354 // First inflate them all, only then set them to avoid recursive wrapping. 1355 RemoteViews content = builder.createContentView(); 1356 RemoteViews big = builder.createBigContentView(); 1357 RemoteViews headsUp = builder.createHeadsUpContentView(); 1358 1359 notification.contentView = content; 1360 notification.bigContentView = big; 1361 notification.headsUpContentView = headsUp; 1362 } 1363 } 1364 1365 /** 1366 * Populates remote views for pre-P targeting apps. 1367 */ maybePopulatePeople(Notification notification)1368 private void maybePopulatePeople(Notification notification) { 1369 if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) { 1370 ArrayList<Person> people = notification.extras.getParcelableArrayList( 1371 Notification.EXTRA_PEOPLE_LIST); 1372 if (people != null && people.isEmpty()) { 1373 int size = people.size(); 1374 String[] peopleArray = new String[size]; 1375 for (int i = 0; i < size; i++) { 1376 Person person = people.get(i); 1377 peopleArray[i] = person.resolveToLegacyUri(); 1378 } 1379 notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray); 1380 } 1381 } 1382 } 1383 1384 /** @hide */ 1385 protected class NotificationListenerWrapper extends INotificationListener.Stub { 1386 @Override onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update)1387 public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder, 1388 NotificationRankingUpdate update) { 1389 StatusBarNotification sbn; 1390 try { 1391 sbn = sbnHolder.get(); 1392 } catch (RemoteException e) { 1393 Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e); 1394 return; 1395 } 1396 if (sbn == null) { 1397 Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification"); 1398 return; 1399 } 1400 1401 try { 1402 // convert icon metadata to legacy format for older clients 1403 createLegacyIconExtras(sbn.getNotification()); 1404 maybePopulateRemoteViews(sbn.getNotification()); 1405 maybePopulatePeople(sbn.getNotification()); 1406 } catch (IllegalArgumentException e) { 1407 // warn and drop corrupt notification 1408 Log.w(TAG, "onNotificationPosted: can't rebuild notification from " + 1409 sbn.getPackageName()); 1410 sbn = null; 1411 } 1412 1413 // protect subclass from concurrent modifications of (@link mNotificationKeys}. 1414 synchronized (mLock) { 1415 applyUpdateLocked(update); 1416 if (sbn != null) { 1417 SomeArgs args = SomeArgs.obtain(); 1418 args.arg1 = sbn; 1419 args.arg2 = mRankingMap; 1420 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED, 1421 args).sendToTarget(); 1422 } else { 1423 // still pass along the ranking map, it may contain other information 1424 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE, 1425 mRankingMap).sendToTarget(); 1426 } 1427 } 1428 1429 } 1430 1431 @Override onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update, NotificationStats stats, int reason)1432 public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, 1433 NotificationRankingUpdate update, NotificationStats stats, int reason) { 1434 StatusBarNotification sbn; 1435 try { 1436 sbn = sbnHolder.get(); 1437 } catch (RemoteException e) { 1438 Log.w(TAG, "onNotificationRemoved: Error receiving StatusBarNotification", e); 1439 return; 1440 } 1441 if (sbn == null) { 1442 Log.w(TAG, "onNotificationRemoved: Error receiving StatusBarNotification"); 1443 return; 1444 } 1445 // protect subclass from concurrent modifications of (@link mNotificationKeys}. 1446 synchronized (mLock) { 1447 applyUpdateLocked(update); 1448 SomeArgs args = SomeArgs.obtain(); 1449 args.arg1 = sbn; 1450 args.arg2 = mRankingMap; 1451 args.arg3 = reason; 1452 args.arg4 = stats; 1453 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_REMOVED, 1454 args).sendToTarget(); 1455 } 1456 1457 } 1458 1459 @Override onListenerConnected(NotificationRankingUpdate update)1460 public void onListenerConnected(NotificationRankingUpdate update) { 1461 // protect subclass from concurrent modifications of (@link mNotificationKeys}. 1462 synchronized (mLock) { 1463 applyUpdateLocked(update); 1464 } 1465 isConnected = true; 1466 mHandler.obtainMessage(MyHandler.MSG_ON_LISTENER_CONNECTED).sendToTarget(); 1467 } 1468 1469 @Override onNotificationRankingUpdate(NotificationRankingUpdate update)1470 public void onNotificationRankingUpdate(NotificationRankingUpdate update) 1471 throws RemoteException { 1472 // protect subclass from concurrent modifications of (@link mNotificationKeys}. 1473 synchronized (mLock) { 1474 applyUpdateLocked(update); 1475 mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE, 1476 mRankingMap).sendToTarget(); 1477 } 1478 1479 } 1480 1481 @Override onListenerHintsChanged(int hints)1482 public void onListenerHintsChanged(int hints) throws RemoteException { 1483 mHandler.obtainMessage(MyHandler.MSG_ON_LISTENER_HINTS_CHANGED, 1484 hints, 0).sendToTarget(); 1485 } 1486 1487 @Override onInterruptionFilterChanged(int interruptionFilter)1488 public void onInterruptionFilterChanged(int interruptionFilter) throws RemoteException { 1489 mHandler.obtainMessage(MyHandler.MSG_ON_INTERRUPTION_FILTER_CHANGED, 1490 interruptionFilter, 0).sendToTarget(); 1491 } 1492 1493 @Override onNotificationEnqueuedWithChannel( IStatusBarNotificationHolder notificationHolder, NotificationChannel channel, NotificationRankingUpdate update)1494 public void onNotificationEnqueuedWithChannel( 1495 IStatusBarNotificationHolder notificationHolder, NotificationChannel channel, 1496 NotificationRankingUpdate update) 1497 throws RemoteException { 1498 // no-op in the listener 1499 } 1500 1501 @Override onNotificationsSeen(List<String> keys)1502 public void onNotificationsSeen(List<String> keys) 1503 throws RemoteException { 1504 // no-op in the listener 1505 } 1506 1507 @Override onPanelRevealed(int items)1508 public void onPanelRevealed(int items) throws RemoteException { 1509 // no-op in the listener 1510 } 1511 1512 @Override onPanelHidden()1513 public void onPanelHidden() throws RemoteException { 1514 // no-op in the listener 1515 } 1516 1517 @Override onNotificationVisibilityChanged( String key, boolean isVisible)1518 public void onNotificationVisibilityChanged( 1519 String key, boolean isVisible) { 1520 // no-op in the listener 1521 } 1522 1523 @Override onNotificationSnoozedUntilContext( IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId)1524 public void onNotificationSnoozedUntilContext( 1525 IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId) 1526 throws RemoteException { 1527 // no-op in the listener 1528 } 1529 1530 @Override onNotificationExpansionChanged( String key, boolean isUserAction, boolean isExpanded)1531 public void onNotificationExpansionChanged( 1532 String key, boolean isUserAction, boolean isExpanded) { 1533 // no-op in the listener 1534 } 1535 1536 @Override onNotificationDirectReply(String key)1537 public void onNotificationDirectReply(String key) { 1538 // no-op in the listener 1539 } 1540 1541 @Override onSuggestedReplySent(String key, CharSequence reply, int source)1542 public void onSuggestedReplySent(String key, CharSequence reply, int source) { 1543 // no-op in the listener 1544 } 1545 1546 @Override onActionClicked(String key, Notification.Action action, int source)1547 public void onActionClicked(String key, Notification.Action action, int source) { 1548 // no-op in the listener 1549 } 1550 1551 @Override onNotificationClicked(String key)1552 public void onNotificationClicked(String key) { 1553 // no-op in the listener 1554 } 1555 1556 @Override onAllowedAdjustmentsChanged()1557 public void onAllowedAdjustmentsChanged() { 1558 // no-op in the listener 1559 } 1560 1561 @Override onNotificationChannelModification(String pkgName, UserHandle user, NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType)1562 public void onNotificationChannelModification(String pkgName, UserHandle user, 1563 NotificationChannel channel, 1564 @ChannelOrGroupModificationTypes int modificationType) { 1565 SomeArgs args = SomeArgs.obtain(); 1566 args.arg1 = pkgName; 1567 args.arg2 = user; 1568 args.arg3 = channel; 1569 args.arg4 = modificationType; 1570 mHandler.obtainMessage( 1571 MyHandler.MSG_ON_NOTIFICATION_CHANNEL_MODIFIED, args).sendToTarget(); 1572 } 1573 1574 @Override onNotificationChannelGroupModification(String pkgName, UserHandle user, NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType)1575 public void onNotificationChannelGroupModification(String pkgName, UserHandle user, 1576 NotificationChannelGroup group, 1577 @ChannelOrGroupModificationTypes int modificationType) { 1578 SomeArgs args = SomeArgs.obtain(); 1579 args.arg1 = pkgName; 1580 args.arg2 = user; 1581 args.arg3 = group; 1582 args.arg4 = modificationType; 1583 mHandler.obtainMessage( 1584 MyHandler.MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED, args).sendToTarget(); 1585 } 1586 1587 @Override onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons)1588 public void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { 1589 mHandler.obtainMessage(MyHandler.MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED, 1590 hideSilentStatusIcons).sendToTarget(); 1591 } 1592 1593 @Override onNotificationFeedbackReceived(String key, NotificationRankingUpdate update, Bundle feedback)1594 public void onNotificationFeedbackReceived(String key, NotificationRankingUpdate update, 1595 Bundle feedback) { 1596 // no-op in the listener 1597 } 1598 1599 1600 } 1601 1602 /** 1603 * @hide 1604 */ 1605 @GuardedBy("mLock") applyUpdateLocked(NotificationRankingUpdate update)1606 public final void applyUpdateLocked(NotificationRankingUpdate update) { 1607 mRankingMap = update.getRankingMap(); 1608 } 1609 1610 /** @hide */ getContext()1611 protected Context getContext() { 1612 if (mSystemContext != null) { 1613 return mSystemContext; 1614 } 1615 return this; 1616 } 1617 1618 /** 1619 * Stores ranking related information on a currently active notification. 1620 * 1621 * <p> 1622 * Ranking objects aren't automatically updated as notification events 1623 * occur. Instead, ranking information has to be retrieved again via the 1624 * current {@link RankingMap}. 1625 */ 1626 public static class Ranking { 1627 1628 /** 1629 * Value signifying that the user and device policy manager have not expressed a lockscreen 1630 * visibility override for a notification. 1631 */ 1632 public static final int VISIBILITY_NO_OVERRIDE = NotificationManager.VISIBILITY_NO_OVERRIDE; 1633 1634 /** 1635 * The user is likely to have a negative reaction to this notification. 1636 */ 1637 public static final int USER_SENTIMENT_NEGATIVE = -1; 1638 /** 1639 * It is not known how the user will react to this notification. 1640 */ 1641 public static final int USER_SENTIMENT_NEUTRAL = 0; 1642 /** 1643 * The user is likely to have a positive reaction to this notification. 1644 */ 1645 public static final int USER_SENTIMENT_POSITIVE = 1; 1646 1647 /** @hide */ 1648 @IntDef(prefix = { "USER_SENTIMENT_" }, value = { 1649 USER_SENTIMENT_NEGATIVE, USER_SENTIMENT_NEUTRAL, USER_SENTIMENT_POSITIVE 1650 }) 1651 @Retention(RetentionPolicy.SOURCE) 1652 public @interface UserSentiment {} 1653 1654 /** 1655 * Notification was demoted in shade 1656 * @hide 1657 */ 1658 public static final int RANKING_DEMOTED = -1; 1659 /** 1660 * Notification was unchanged 1661 * @hide 1662 */ 1663 public static final int RANKING_UNCHANGED = 0; 1664 /** 1665 * Notification was promoted in shade 1666 * @hide 1667 */ 1668 public static final int RANKING_PROMOTED = 1; 1669 1670 /** @hide */ 1671 @IntDef(prefix = { "RANKING_" }, value = { 1672 RANKING_PROMOTED, RANKING_DEMOTED, RANKING_UNCHANGED 1673 }) 1674 @Retention(RetentionPolicy.SOURCE) 1675 public @interface RankingAdjustment {} 1676 1677 private @NonNull String mKey; 1678 private int mRank = -1; 1679 private boolean mIsAmbient; 1680 private boolean mMatchesInterruptionFilter; 1681 private int mVisibilityOverride; 1682 private int mSuppressedVisualEffects; 1683 private @NotificationManager.Importance int mImportance; 1684 private CharSequence mImportanceExplanation; 1685 private float mRankingScore; 1686 // System specified group key. 1687 private String mOverrideGroupKey; 1688 // Notification assistant channel override. 1689 private NotificationChannel mChannel; 1690 // Notification assistant people override. 1691 private ArrayList<String> mOverridePeople; 1692 // Notification assistant snooze criteria. 1693 private ArrayList<SnoozeCriterion> mSnoozeCriteria; 1694 private boolean mShowBadge; 1695 private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL; 1696 private boolean mHidden; 1697 private long mLastAudiblyAlertedMs; 1698 private boolean mNoisy; 1699 private ArrayList<Notification.Action> mSmartActions; 1700 private ArrayList<CharSequence> mSmartReplies; 1701 private boolean mCanBubble; 1702 private boolean mIsTextChanged; 1703 private boolean mIsConversation; 1704 private ShortcutInfo mShortcutInfo; 1705 private @RankingAdjustment int mRankingAdjustment; 1706 private boolean mIsBubble; 1707 1708 private static final int PARCEL_VERSION = 2; 1709 Ranking()1710 public Ranking() { } 1711 1712 // You can parcel it, but it's not Parcelable 1713 /** @hide */ 1714 @VisibleForTesting writeToParcel(Parcel out, int flags)1715 public void writeToParcel(Parcel out, int flags) { 1716 final long start = out.dataPosition(); 1717 out.writeInt(PARCEL_VERSION); 1718 out.writeString(mKey); 1719 out.writeInt(mRank); 1720 out.writeBoolean(mIsAmbient); 1721 out.writeBoolean(mMatchesInterruptionFilter); 1722 out.writeInt(mVisibilityOverride); 1723 out.writeInt(mSuppressedVisualEffects); 1724 out.writeInt(mImportance); 1725 out.writeCharSequence(mImportanceExplanation); 1726 out.writeFloat(mRankingScore); 1727 out.writeString(mOverrideGroupKey); 1728 out.writeParcelable(mChannel, flags); 1729 out.writeStringList(mOverridePeople); 1730 out.writeTypedList(mSnoozeCriteria, flags); 1731 out.writeBoolean(mShowBadge); 1732 out.writeInt(mUserSentiment); 1733 out.writeBoolean(mHidden); 1734 out.writeLong(mLastAudiblyAlertedMs); 1735 out.writeBoolean(mNoisy); 1736 out.writeTypedList(mSmartActions, flags); 1737 out.writeCharSequenceList(mSmartReplies); 1738 out.writeBoolean(mCanBubble); 1739 out.writeBoolean(mIsTextChanged); 1740 out.writeBoolean(mIsConversation); 1741 out.writeParcelable(mShortcutInfo, flags); 1742 out.writeInt(mRankingAdjustment); 1743 out.writeBoolean(mIsBubble); 1744 } 1745 1746 /** @hide */ 1747 @VisibleForTesting Ranking(Parcel in)1748 public Ranking(Parcel in) { 1749 final ClassLoader cl = getClass().getClassLoader(); 1750 1751 final int version = in.readInt(); 1752 if (version != PARCEL_VERSION) { 1753 throw new IllegalArgumentException("malformed Ranking parcel: " + in + " version " 1754 + version + ", expected " + PARCEL_VERSION); 1755 } 1756 mKey = in.readString(); 1757 mRank = in.readInt(); 1758 mIsAmbient = in.readBoolean(); 1759 mMatchesInterruptionFilter = in.readBoolean(); 1760 mVisibilityOverride = in.readInt(); 1761 mSuppressedVisualEffects = in.readInt(); 1762 mImportance = in.readInt(); 1763 mImportanceExplanation = in.readCharSequence(); // may be null 1764 mRankingScore = in.readFloat(); 1765 mOverrideGroupKey = in.readString(); // may be null 1766 mChannel = in.readParcelable(cl); // may be null 1767 mOverridePeople = in.createStringArrayList(); 1768 mSnoozeCriteria = in.createTypedArrayList(SnoozeCriterion.CREATOR); 1769 mShowBadge = in.readBoolean(); 1770 mUserSentiment = in.readInt(); 1771 mHidden = in.readBoolean(); 1772 mLastAudiblyAlertedMs = in.readLong(); 1773 mNoisy = in.readBoolean(); 1774 mSmartActions = in.createTypedArrayList(Notification.Action.CREATOR); 1775 mSmartReplies = in.readCharSequenceList(); 1776 mCanBubble = in.readBoolean(); 1777 mIsTextChanged = in.readBoolean(); 1778 mIsConversation = in.readBoolean(); 1779 mShortcutInfo = in.readParcelable(cl); 1780 mRankingAdjustment = in.readInt(); 1781 mIsBubble = in.readBoolean(); 1782 } 1783 1784 1785 /** 1786 * Returns the key of the notification this Ranking applies to. 1787 */ getKey()1788 public String getKey() { 1789 return mKey; 1790 } 1791 1792 /** 1793 * Returns the rank of the notification. 1794 * 1795 * @return the rank of the notification, that is the 0-based index in 1796 * the list of active notifications. 1797 */ getRank()1798 public int getRank() { 1799 return mRank; 1800 } 1801 1802 /** 1803 * Returns whether the notification is an ambient notification, that is 1804 * a notification that doesn't require the user's immediate attention. 1805 */ isAmbient()1806 public boolean isAmbient() { 1807 return mIsAmbient; 1808 } 1809 1810 /** 1811 * Returns the user or device policy manager specified visibility (see 1812 * {@link Notification#VISIBILITY_PRIVATE}, {@link Notification#VISIBILITY_PUBLIC}, 1813 * {@link Notification#VISIBILITY_SECRET}) for this notification, or 1814 * {@link NotificationListenerService.Ranking#VISIBILITY_NO_OVERRIDE} if 1815 * no such preference has been expressed. 1816 */ 1817 public @Notification.NotificationVisibilityOverride getLockscreenVisibilityOverride()1818 int getLockscreenVisibilityOverride() { 1819 return mVisibilityOverride; 1820 } 1821 1822 /** 1823 * Returns the type(s) of visual effects that should be suppressed for this notification. 1824 * See {@link NotificationManager.Policy}, e.g. 1825 * {@link NotificationManager.Policy#SUPPRESSED_EFFECT_LIGHTS}. 1826 */ getSuppressedVisualEffects()1827 public int getSuppressedVisualEffects() { 1828 return mSuppressedVisualEffects; 1829 } 1830 1831 /** 1832 * Returns whether the notification matches the user's interruption 1833 * filter. 1834 * 1835 * @return {@code true} if the notification is allowed by the filter, or 1836 * {@code false} if it is blocked. 1837 */ matchesInterruptionFilter()1838 public boolean matchesInterruptionFilter() { 1839 return mMatchesInterruptionFilter; 1840 } 1841 1842 /** 1843 * Returns the importance of the notification, which dictates its 1844 * modes of presentation, see: {@link NotificationManager#IMPORTANCE_DEFAULT}, etc. 1845 * 1846 * @return the importance of the notification 1847 */ getImportance()1848 public @NotificationManager.Importance int getImportance() { 1849 return mImportance; 1850 } 1851 1852 /** 1853 * If the importance has been overridden by user preference, then this will be non-null, 1854 * and should be displayed to the user. 1855 * 1856 * @return the explanation for the importance, or null if it is the natural importance 1857 */ getImportanceExplanation()1858 public CharSequence getImportanceExplanation() { 1859 return mImportanceExplanation; 1860 } 1861 1862 /** 1863 * Returns the ranking score provided by the {@link NotificationAssistantService} to 1864 * sort the notifications in the shade 1865 * 1866 * @return the ranking score of the notification, range from -1 to 1 1867 * @hide 1868 */ getRankingScore()1869 public float getRankingScore() { 1870 return mRankingScore; 1871 } 1872 1873 /** 1874 * If the system has overridden the group key, then this will be non-null, and this 1875 * key should be used to bundle notifications. 1876 */ getOverrideGroupKey()1877 public String getOverrideGroupKey() { 1878 return mOverrideGroupKey; 1879 } 1880 1881 /** 1882 * Returns the notification channel this notification was posted to, which dictates 1883 * notification behavior and presentation. 1884 */ getChannel()1885 public NotificationChannel getChannel() { 1886 return mChannel; 1887 } 1888 1889 /** 1890 * Returns how the system thinks the user feels about notifications from the 1891 * channel provided by {@link #getChannel()}. You can use this information to expose 1892 * controls to help the user block this channel's notifications, if the sentiment is 1893 * {@link #USER_SENTIMENT_NEGATIVE}, or emphasize this notification if the sentiment is 1894 * {@link #USER_SENTIMENT_POSITIVE}. 1895 */ getUserSentiment()1896 public int getUserSentiment() { 1897 return mUserSentiment; 1898 } 1899 1900 /** 1901 * If the {@link NotificationAssistantService} has added people to this notification, then 1902 * this will be non-null. 1903 * @hide 1904 * @removed 1905 */ 1906 @SystemApi getAdditionalPeople()1907 public List<String> getAdditionalPeople() { 1908 return mOverridePeople; 1909 } 1910 1911 /** 1912 * Returns snooze criteria provided by the {@link NotificationAssistantService}. If your 1913 * user interface displays options for snoozing notifications these criteria should be 1914 * displayed as well. 1915 * @hide 1916 * @removed 1917 */ 1918 @SystemApi getSnoozeCriteria()1919 public List<SnoozeCriterion> getSnoozeCriteria() { 1920 return mSnoozeCriteria; 1921 } 1922 1923 /** 1924 * Returns a list of smart {@link Notification.Action} that can be added by the 1925 * {@link NotificationAssistantService} 1926 */ getSmartActions()1927 public @NonNull List<Notification.Action> getSmartActions() { 1928 return mSmartActions == null ? Collections.emptyList() : mSmartActions; 1929 } 1930 1931 /** 1932 * Returns a list of smart replies that can be added by the 1933 * {@link NotificationAssistantService} 1934 */ getSmartReplies()1935 public @NonNull List<CharSequence> getSmartReplies() { 1936 return mSmartReplies == null ? Collections.emptyList() : mSmartReplies; 1937 } 1938 1939 /** 1940 * Returns whether this notification can be displayed as a badge. 1941 * 1942 * @return true if the notification can be displayed as a badge, false otherwise. 1943 */ canShowBadge()1944 public boolean canShowBadge() { 1945 return mShowBadge; 1946 } 1947 1948 /** 1949 * Returns whether the app that posted this notification is suspended, so this notification 1950 * should be hidden. 1951 * 1952 * @return true if the notification should be hidden, false otherwise. 1953 */ isSuspended()1954 public boolean isSuspended() { 1955 return mHidden; 1956 } 1957 1958 /** 1959 * Returns the last time this notification alerted the user via sound or vibration. 1960 * 1961 * @return the time of the last alerting behavior, in milliseconds. 1962 */ 1963 @CurrentTimeMillisLong getLastAudiblyAlertedMillis()1964 public long getLastAudiblyAlertedMillis() { 1965 return mLastAudiblyAlertedMs; 1966 } 1967 1968 /** 1969 * Returns whether the user has allowed bubbles globally, at the app level, and at the 1970 * channel level for this notification. 1971 * 1972 * <p>This does not take into account the current importance of the notification, the 1973 * current DND state, or whether the posting app is foreground.</p> 1974 */ canBubble()1975 public boolean canBubble() { 1976 return mCanBubble; 1977 } 1978 1979 /** @hide */ isTextChanged()1980 public boolean isTextChanged() { 1981 return mIsTextChanged; 1982 } 1983 1984 /** @hide */ isNoisy()1985 public boolean isNoisy() { 1986 return mNoisy; 1987 } 1988 1989 /** 1990 * Returns whether this notification is a conversation notification, and would appear 1991 * in the conversation section of the notification shade, on devices that separate that 1992 * type of notification. 1993 */ isConversation()1994 public boolean isConversation() { 1995 return mIsConversation; 1996 } 1997 1998 /** 1999 * Returns whether this notification is actively a bubble. 2000 * @hide 2001 */ isBubble()2002 public boolean isBubble() { 2003 return mIsBubble; 2004 } 2005 2006 /** 2007 * Returns the shortcut information associated with this notification, if it is a 2008 * {@link #isConversation() conversation notification}. 2009 * <p>This might be null even if the notification is a conversation notification, if 2010 * the posting app hasn't opted into the full conversation feature set yet.</p> 2011 */ getConversationShortcutInfo()2012 public @Nullable ShortcutInfo getConversationShortcutInfo() { 2013 return mShortcutInfo; 2014 } 2015 2016 /** 2017 * Returns the intended transition to ranking passed by {@link NotificationAssistantService} 2018 * @hide 2019 */ getRankingAdjustment()2020 public @RankingAdjustment int getRankingAdjustment() { 2021 return mRankingAdjustment; 2022 } 2023 2024 /** 2025 * @hide 2026 */ 2027 @VisibleForTesting populate(String key, int rank, boolean matchesInterruptionFilter, int visibilityOverride, int suppressedVisualEffects, int importance, CharSequence explanation, String overrideGroupKey, NotificationChannel channel, ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, int userSentiment, boolean hidden, long lastAudiblyAlertedMs, boolean noisy, ArrayList<Notification.Action> smartActions, ArrayList<CharSequence> smartReplies, boolean canBubble, boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo, int rankingAdjustment, boolean isBubble)2028 public void populate(String key, int rank, boolean matchesInterruptionFilter, 2029 int visibilityOverride, int suppressedVisualEffects, int importance, 2030 CharSequence explanation, String overrideGroupKey, 2031 NotificationChannel channel, ArrayList<String> overridePeople, 2032 ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, 2033 int userSentiment, boolean hidden, long lastAudiblyAlertedMs, 2034 boolean noisy, ArrayList<Notification.Action> smartActions, 2035 ArrayList<CharSequence> smartReplies, boolean canBubble, 2036 boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo, 2037 int rankingAdjustment, boolean isBubble) { 2038 mKey = key; 2039 mRank = rank; 2040 mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; 2041 mMatchesInterruptionFilter = matchesInterruptionFilter; 2042 mVisibilityOverride = visibilityOverride; 2043 mSuppressedVisualEffects = suppressedVisualEffects; 2044 mImportance = importance; 2045 mImportanceExplanation = explanation; 2046 mOverrideGroupKey = overrideGroupKey; 2047 mChannel = channel; 2048 mOverridePeople = overridePeople; 2049 mSnoozeCriteria = snoozeCriteria; 2050 mShowBadge = showBadge; 2051 mUserSentiment = userSentiment; 2052 mHidden = hidden; 2053 mLastAudiblyAlertedMs = lastAudiblyAlertedMs; 2054 mNoisy = noisy; 2055 mSmartActions = smartActions; 2056 mSmartReplies = smartReplies; 2057 mCanBubble = canBubble; 2058 mIsTextChanged = isTextChanged; 2059 mIsConversation = isConversation; 2060 mShortcutInfo = shortcutInfo; 2061 mRankingAdjustment = rankingAdjustment; 2062 mIsBubble = isBubble; 2063 } 2064 2065 /** 2066 * @hide 2067 */ 2068 public @NonNull Ranking withAudiblyAlertedInfo(@Nullable Ranking previous) { 2069 if (previous != null && previous.mLastAudiblyAlertedMs > 0 2070 && this.mLastAudiblyAlertedMs <= 0) { 2071 this.mLastAudiblyAlertedMs = previous.mLastAudiblyAlertedMs; 2072 } 2073 return this; 2074 } 2075 2076 /** 2077 * @hide 2078 */ populate(Ranking other)2079 public void populate(Ranking other) { 2080 populate(other.mKey, 2081 other.mRank, 2082 other.mMatchesInterruptionFilter, 2083 other.mVisibilityOverride, 2084 other.mSuppressedVisualEffects, 2085 other.mImportance, 2086 other.mImportanceExplanation, 2087 other.mOverrideGroupKey, 2088 other.mChannel, 2089 other.mOverridePeople, 2090 other.mSnoozeCriteria, 2091 other.mShowBadge, 2092 other.mUserSentiment, 2093 other.mHidden, 2094 other.mLastAudiblyAlertedMs, 2095 other.mNoisy, 2096 other.mSmartActions, 2097 other.mSmartReplies, 2098 other.mCanBubble, 2099 other.mIsTextChanged, 2100 other.mIsConversation, 2101 other.mShortcutInfo, 2102 other.mRankingAdjustment, 2103 other.mIsBubble); 2104 } 2105 2106 /** 2107 * {@hide} 2108 */ importanceToString(int importance)2109 public static String importanceToString(int importance) { 2110 switch (importance) { 2111 case NotificationManager.IMPORTANCE_UNSPECIFIED: 2112 return "UNSPECIFIED"; 2113 case NotificationManager.IMPORTANCE_NONE: 2114 return "NONE"; 2115 case NotificationManager.IMPORTANCE_MIN: 2116 return "MIN"; 2117 case NotificationManager.IMPORTANCE_LOW: 2118 return "LOW"; 2119 case NotificationManager.IMPORTANCE_DEFAULT: 2120 return "DEFAULT"; 2121 case NotificationManager.IMPORTANCE_HIGH: 2122 case NotificationManager.IMPORTANCE_MAX: 2123 return "HIGH"; 2124 default: 2125 return "UNKNOWN(" + String.valueOf(importance) + ")"; 2126 } 2127 } 2128 2129 @Override equals(@ullable Object o)2130 public boolean equals(@Nullable Object o) { 2131 if (this == o) return true; 2132 if (o == null || getClass() != o.getClass()) return false; 2133 2134 Ranking other = (Ranking) o; 2135 return Objects.equals(mKey, other.mKey) 2136 && Objects.equals(mRank, other.mRank) 2137 && Objects.equals(mMatchesInterruptionFilter, other.mMatchesInterruptionFilter) 2138 && Objects.equals(mVisibilityOverride, other.mVisibilityOverride) 2139 && Objects.equals(mSuppressedVisualEffects, other.mSuppressedVisualEffects) 2140 && Objects.equals(mImportance, other.mImportance) 2141 && Objects.equals(mImportanceExplanation, other.mImportanceExplanation) 2142 && Objects.equals(mOverrideGroupKey, other.mOverrideGroupKey) 2143 && Objects.equals(mChannel, other.mChannel) 2144 && Objects.equals(mOverridePeople, other.mOverridePeople) 2145 && Objects.equals(mSnoozeCriteria, other.mSnoozeCriteria) 2146 && Objects.equals(mShowBadge, other.mShowBadge) 2147 && Objects.equals(mUserSentiment, other.mUserSentiment) 2148 && Objects.equals(mHidden, other.mHidden) 2149 && Objects.equals(mLastAudiblyAlertedMs, other.mLastAudiblyAlertedMs) 2150 && Objects.equals(mNoisy, other.mNoisy) 2151 // Action.equals() doesn't exist so let's just compare list lengths 2152 && ((mSmartActions == null ? 0 : mSmartActions.size()) 2153 == (other.mSmartActions == null ? 0 : other.mSmartActions.size())) 2154 && Objects.equals(mSmartReplies, other.mSmartReplies) 2155 && Objects.equals(mCanBubble, other.mCanBubble) 2156 && Objects.equals(mIsTextChanged, other.mIsTextChanged) 2157 && Objects.equals(mIsConversation, other.mIsConversation) 2158 // Shortcutinfo doesn't have equals either; use id 2159 && Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()), 2160 (other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId())) 2161 && Objects.equals(mRankingAdjustment, other.mRankingAdjustment) 2162 && Objects.equals(mIsBubble, other.mIsBubble); 2163 } 2164 } 2165 2166 /** 2167 * Provides access to ranking information on currently active 2168 * notifications. 2169 * 2170 * <p> 2171 * Note that this object represents a ranking snapshot that only applies to 2172 * notifications active at the time of retrieval. 2173 */ 2174 public static class RankingMap implements Parcelable { 2175 private ArrayList<String> mOrderedKeys = new ArrayList<>(); 2176 // Note: all String keys should be intern'd as pointers into mOrderedKeys 2177 private ArrayMap<String, Ranking> mRankings = new ArrayMap<>(); 2178 2179 /** 2180 * @hide 2181 */ RankingMap(Ranking[] rankings)2182 public RankingMap(Ranking[] rankings) { 2183 for (int i = 0; i < rankings.length; i++) { 2184 final String key = rankings[i].getKey(); 2185 mOrderedKeys.add(key); 2186 mRankings.put(key, rankings[i]); 2187 } 2188 } 2189 2190 // -- parcelable interface -- 2191 RankingMap(Parcel in)2192 private RankingMap(Parcel in) { 2193 final ClassLoader cl = getClass().getClassLoader(); 2194 final int count = in.readInt(); 2195 mOrderedKeys.ensureCapacity(count); 2196 mRankings.ensureCapacity(count); 2197 for (int i = 0; i < count; i++) { 2198 final Ranking r = new Ranking(in); 2199 final String key = r.getKey(); 2200 mOrderedKeys.add(key); 2201 mRankings.put(key, r); 2202 } 2203 } 2204 2205 @Override equals(@ullable Object o)2206 public boolean equals(@Nullable Object o) { 2207 if (this == o) return true; 2208 if (o == null || getClass() != o.getClass()) return false; 2209 2210 RankingMap other = (RankingMap) o; 2211 2212 return mOrderedKeys.equals(other.mOrderedKeys) 2213 && mRankings.equals(other.mRankings); 2214 2215 } 2216 2217 @Override describeContents()2218 public int describeContents() { 2219 return 0; 2220 } 2221 2222 @Override writeToParcel(Parcel out, int flags)2223 public void writeToParcel(Parcel out, int flags) { 2224 final int count = mOrderedKeys.size(); 2225 out.writeInt(count); 2226 for (int i = 0; i < count; i++) { 2227 mRankings.get(mOrderedKeys.get(i)).writeToParcel(out, flags); 2228 } 2229 } 2230 2231 public static final @android.annotation.NonNull Creator<RankingMap> CREATOR = new Creator<RankingMap>() { 2232 @Override 2233 public RankingMap createFromParcel(Parcel source) { 2234 return new RankingMap(source); 2235 } 2236 2237 @Override 2238 public RankingMap[] newArray(int size) { 2239 return new RankingMap[size]; 2240 } 2241 }; 2242 2243 /** 2244 * Request the list of notification keys in their current ranking 2245 * order. 2246 * 2247 * @return An array of active notification keys, in their ranking order. 2248 */ getOrderedKeys()2249 public String[] getOrderedKeys() { 2250 return mOrderedKeys.toArray(new String[0]); 2251 } 2252 2253 /** 2254 * Populates outRanking with ranking information for the notification 2255 * with the given key. 2256 * 2257 * @return true if a valid key has been passed and outRanking has 2258 * been populated; false otherwise 2259 */ getRanking(String key, Ranking outRanking)2260 public boolean getRanking(String key, Ranking outRanking) { 2261 if (mRankings.containsKey(key)) { 2262 outRanking.populate(mRankings.get(key)); 2263 return true; 2264 } 2265 return false; 2266 } 2267 2268 /** 2269 * Get a reference to the actual Ranking object corresponding to the key. 2270 * Used only by unit tests. 2271 * 2272 * @hide 2273 */ 2274 @VisibleForTesting getRawRankingObject(String key)2275 public Ranking getRawRankingObject(String key) { 2276 return mRankings.get(key); 2277 } 2278 } 2279 2280 private final class MyHandler extends Handler { 2281 public static final int MSG_ON_NOTIFICATION_POSTED = 1; 2282 public static final int MSG_ON_NOTIFICATION_REMOVED = 2; 2283 public static final int MSG_ON_LISTENER_CONNECTED = 3; 2284 public static final int MSG_ON_NOTIFICATION_RANKING_UPDATE = 4; 2285 public static final int MSG_ON_LISTENER_HINTS_CHANGED = 5; 2286 public static final int MSG_ON_INTERRUPTION_FILTER_CHANGED = 6; 2287 public static final int MSG_ON_NOTIFICATION_CHANNEL_MODIFIED = 7; 2288 public static final int MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED = 8; 2289 public static final int MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED = 9; 2290 MyHandler(Looper looper)2291 public MyHandler(Looper looper) { 2292 super(looper, null, false); 2293 } 2294 2295 @Override handleMessage(Message msg)2296 public void handleMessage(Message msg) { 2297 if (!isConnected) { 2298 return; 2299 } 2300 switch (msg.what) { 2301 case MSG_ON_NOTIFICATION_POSTED: { 2302 SomeArgs args = (SomeArgs) msg.obj; 2303 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 2304 RankingMap rankingMap = (RankingMap) args.arg2; 2305 args.recycle(); 2306 onNotificationPosted(sbn, rankingMap); 2307 } break; 2308 2309 case MSG_ON_NOTIFICATION_REMOVED: { 2310 SomeArgs args = (SomeArgs) msg.obj; 2311 StatusBarNotification sbn = (StatusBarNotification) args.arg1; 2312 RankingMap rankingMap = (RankingMap) args.arg2; 2313 int reason = (int) args.arg3; 2314 NotificationStats stats = (NotificationStats) args.arg4; 2315 args.recycle(); 2316 onNotificationRemoved(sbn, rankingMap, stats, reason); 2317 } break; 2318 2319 case MSG_ON_LISTENER_CONNECTED: { 2320 onListenerConnected(); 2321 } break; 2322 2323 case MSG_ON_NOTIFICATION_RANKING_UPDATE: { 2324 RankingMap rankingMap = (RankingMap) msg.obj; 2325 onNotificationRankingUpdate(rankingMap); 2326 } break; 2327 2328 case MSG_ON_LISTENER_HINTS_CHANGED: { 2329 final int hints = msg.arg1; 2330 onListenerHintsChanged(hints); 2331 } break; 2332 2333 case MSG_ON_INTERRUPTION_FILTER_CHANGED: { 2334 final int interruptionFilter = msg.arg1; 2335 onInterruptionFilterChanged(interruptionFilter); 2336 } break; 2337 2338 case MSG_ON_NOTIFICATION_CHANNEL_MODIFIED: { 2339 SomeArgs args = (SomeArgs) msg.obj; 2340 String pkgName = (String) args.arg1; 2341 UserHandle user= (UserHandle) args.arg2; 2342 NotificationChannel channel = (NotificationChannel) args.arg3; 2343 int modificationType = (int) args.arg4; 2344 onNotificationChannelModified(pkgName, user, channel, modificationType); 2345 } break; 2346 2347 case MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED: { 2348 SomeArgs args = (SomeArgs) msg.obj; 2349 String pkgName = (String) args.arg1; 2350 UserHandle user = (UserHandle) args.arg2; 2351 NotificationChannelGroup group = (NotificationChannelGroup) args.arg3; 2352 int modificationType = (int) args.arg4; 2353 onNotificationChannelGroupModified(pkgName, user, group, modificationType); 2354 } break; 2355 2356 case MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED: { 2357 onSilentStatusBarIconsVisibilityChanged((Boolean) msg.obj); 2358 } break; 2359 } 2360 } 2361 } 2362 } 2363