1 /* 2 * Copyright (C) 2016 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 package com.android.server.notification; 17 18 import static android.app.Notification.COLOR_DEFAULT; 19 import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; 20 import static android.app.Notification.FLAG_AUTO_CANCEL; 21 import static android.app.Notification.FLAG_GROUP_SUMMARY; 22 import static android.app.Notification.FLAG_LOCAL_ONLY; 23 import static android.app.Notification.FLAG_NO_CLEAR; 24 import static android.app.Notification.FLAG_ONGOING_EVENT; 25 import static android.app.Notification.VISIBILITY_PRIVATE; 26 import static android.app.Notification.VISIBILITY_PUBLIC; 27 import static android.service.notification.Flags.notificationForceGrouping; 28 import static android.service.notification.Flags.notificationRegroupOnClassification; 29 30 import android.annotation.FlaggedApi; 31 import android.annotation.IntDef; 32 import android.annotation.NonNull; 33 import android.annotation.Nullable; 34 import android.app.ActivityManager; 35 import android.app.Notification; 36 import android.app.NotificationChannel; 37 import android.app.NotificationManager; 38 import android.app.PendingIntent; 39 import android.content.Context; 40 import android.content.pm.PackageManager; 41 import android.content.pm.PackageManager.NameNotFoundException; 42 import android.graphics.drawable.AdaptiveIconDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.graphics.drawable.Icon; 45 import android.service.notification.StatusBarNotification; 46 import android.text.TextUtils; 47 import android.util.ArrayMap; 48 import android.util.Log; 49 import android.util.Slog; 50 51 import com.android.internal.R; 52 import com.android.internal.annotations.GuardedBy; 53 import com.android.internal.annotations.VisibleForTesting; 54 55 import java.io.PrintWriter; 56 import java.lang.annotation.Retention; 57 import java.lang.annotation.RetentionPolicy; 58 import java.util.ArrayList; 59 import java.util.Collection; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Map.Entry; 64 import java.util.Objects; 65 import java.util.Set; 66 import java.util.function.Predicate; 67 68 /** 69 * NotificationManagerService helper for auto-grouping notifications. 70 */ 71 public class GroupHelper { 72 private static final String TAG = "GroupHelper"; 73 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 74 75 protected static final String AUTOGROUP_KEY = "ranker_group"; 76 77 protected static final int FLAG_INVALID = -1; 78 79 // Flags that all autogroup summaries have 80 protected static final int BASE_FLAGS = 81 FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY; 82 // Flag that autogroup summaries inherits if all children have the flag 83 private static final int ALL_CHILDREN_FLAG = FLAG_AUTO_CANCEL; 84 // Flags that autogroup summaries inherits if any child has them 85 private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; 86 87 protected static final String AGGREGATE_GROUP_KEY = "Aggregate_"; 88 89 // If an app posts more than NotificationManagerService.AUTOGROUP_SPARSE_GROUPS_AT_COUNT groups 90 // with less than this value, they will be forced grouped 91 private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3; 92 93 // Regrouping needed because the channel was updated, ie. importance changed 94 static final int REGROUP_REASON_CHANNEL_UPDATE = 0; 95 // Regrouping needed because of notification bundling 96 static final int REGROUP_REASON_BUNDLE = 1; 97 // Regrouping needed because of notification unbundling 98 static final int REGROUP_REASON_UNBUNDLE = 2; 99 // Regrouping needed because of notification unbundling + the original group summary exists 100 static final int REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP = 3; 101 102 @IntDef(prefix = { "REGROUP_REASON_" }, value = { 103 REGROUP_REASON_CHANNEL_UPDATE, 104 REGROUP_REASON_BUNDLE, 105 REGROUP_REASON_UNBUNDLE, 106 REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP, 107 }) 108 @Retention(RetentionPolicy.SOURCE) 109 @interface RegroupingReason {} 110 111 private final Callback mCallback; 112 private final int mAutoGroupAtCount; 113 private final int mAutogroupSparseGroupsAtCount; 114 private final Context mContext; 115 private final PackageManager mPackageManager; 116 private boolean mIsTestHarnessExempted; 117 118 // Only contains notifications that are not explicitly grouped by the app (aka no group or 119 // sort key). 120 // userId|packageName -> (keys of notifications that aren't in an explicit app group -> flags) 121 @GuardedBy("mUngroupedNotifications") 122 private final ArrayMap<String, ArrayMap<String, NotificationAttributes>> mUngroupedNotifications 123 = new ArrayMap<>(); 124 125 // Contains the list of notifications that should be aggregated (forced grouping) 126 // but there are less than mAutoGroupAtCount per section for a package. 127 // The primary map's key is the full aggregated group key: userId|pkgName|g:groupName 128 // The internal map's key is the notification record key 129 @GuardedBy("mAggregatedNotifications") 130 private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> 131 mUngroupedAbuseNotifications = new ArrayMap<>(); 132 133 // Contains the list of group summaries that were canceled when "singleton groups" were 134 // force grouped. Key is userId|packageName|g:OriginalGroupName. Used to: 135 // 1) remove the original group's children when an app cancels the already removed summary. 136 // 2) perform the same side effects that would happen if the group is removed because 137 // all its force-regrouped children are removed (e.g. firing its deleteIntent). 138 @GuardedBy("mAggregatedNotifications") 139 private final ArrayMap<FullyQualifiedGroupKey, CachedSummary> 140 mCanceledSummaries = new ArrayMap<>(); 141 142 // Represents the current state of the aggregated (forced grouped) notifications 143 // Key is the full aggregated group key: userId|pkgName|g:groupName 144 // And groupName is "Aggregate_"+sectionName 145 @GuardedBy("mAggregatedNotifications") 146 private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> 147 mAggregatedNotifications = new ArrayMap<>(); 148 149 private static List<NotificationSectioner> NOTIFICATION_SHADE_SECTIONS = 150 getNotificationShadeSections(); 151 152 private static List<NotificationSectioner> NOTIFICATION_BUNDLE_SECTIONS; 153 getNotificationShadeSections()154 private static List<NotificationSectioner> getNotificationShadeSections() { 155 ArrayList<NotificationSectioner> sectionsList = new ArrayList<>(); 156 if (android.service.notification.Flags.notificationClassification()) { 157 sectionsList.addAll(List.of( 158 new NotificationSectioner("PromotionsSection", 0, (record) -> 159 NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId()) 160 && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), 161 new NotificationSectioner("SocialSection", 0, (record) -> 162 NotificationChannel.SOCIAL_MEDIA_ID.equals(record.getChannel().getId()) 163 && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), 164 new NotificationSectioner("NewsSection", 0, (record) -> 165 NotificationChannel.NEWS_ID.equals(record.getChannel().getId()) 166 && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), 167 new NotificationSectioner("RecsSection", 0, (record) -> 168 NotificationChannel.RECS_ID.equals(record.getChannel().getId()) 169 && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) 170 )); 171 172 NOTIFICATION_BUNDLE_SECTIONS = new ArrayList<>(sectionsList); 173 } 174 175 if (Flags.notificationForceGroupConversations()) { 176 // add priority people section 177 sectionsList.add(new NotificationSectioner("PeopleSection(priority)", 1, (record) -> 178 record.isConversation() && record.getChannel().isImportantConversation())); 179 180 if (android.app.Flags.sortSectionByTime()) { 181 // add single people (alerting) section 182 sectionsList.add(new NotificationSectioner("PeopleSection", 0, 183 NotificationRecord::isConversation)); 184 } else { 185 // add people alerting section 186 sectionsList.add(new NotificationSectioner("PeopleSection(alerting)", 1, (record) -> 187 record.isConversation() 188 && record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT)); 189 // add people silent section 190 sectionsList.add(new NotificationSectioner("PeopleSection(silent)", 1, (record) -> 191 record.isConversation() 192 && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)); 193 } 194 } 195 196 sectionsList.addAll(List.of( 197 new NotificationSectioner("AlertingSection", 0, (record) -> 198 record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT), 199 new NotificationSectioner("SilentSection", 1, (record) -> 200 record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT))); 201 return sectionsList; 202 } 203 GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, int autoGroupSparseGroupsAtCount, Callback callback)204 public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, 205 int autoGroupSparseGroupsAtCount, Callback callback) { 206 mAutoGroupAtCount = autoGroupAtCount; 207 mCallback = callback; 208 mContext = context; 209 mPackageManager = packageManager; 210 mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount; 211 NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections(); 212 } 213 setTestHarnessExempted(boolean isExempted)214 void setTestHarnessExempted(boolean isExempted) { 215 // Allow E2E tests to post ungrouped notifications 216 mIsTestHarnessExempted = ActivityManager.isRunningInUserTestHarness() && isExempted; 217 } 218 generatePackageKey(int userId, String pkg)219 private String generatePackageKey(int userId, String pkg) { 220 return userId + "|" + pkg; 221 } 222 223 @VisibleForTesting getAutogroupSummaryFlags( @onNull final ArrayMap<String, NotificationAttributes> childrenMap)224 protected static int getAutogroupSummaryFlags( 225 @NonNull final ArrayMap<String, NotificationAttributes> childrenMap) { 226 final Collection<NotificationAttributes> children = childrenMap.values(); 227 boolean allChildrenHasFlag = children.size() > 0; 228 int anyChildFlagSet = 0; 229 for (NotificationAttributes childAttr: children) { 230 if (!hasAnyFlag(childAttr.flags, ALL_CHILDREN_FLAG)) { 231 allChildrenHasFlag = false; 232 } 233 if (hasAnyFlag(childAttr.flags, ANY_CHILDREN_FLAGS)) { 234 anyChildFlagSet |= (childAttr.flags & ANY_CHILDREN_FLAGS); 235 } 236 } 237 return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet; 238 } 239 hasAnyFlag(int flags, int mask)240 private static boolean hasAnyFlag(int flags, int mask) { 241 return (flags & mask) != 0; 242 } 243 244 /** 245 * Called when a notification is newly posted. Checks whether that notification, and all other 246 * active notifications should be grouped or ungrouped atuomatically, and returns whether. 247 * @param record The posted notification. 248 * @param autogroupSummaryExists Whether a summary for this notification already exists. 249 * @return Whether the provided notification should be autogrouped synchronously. 250 */ onNotificationPosted(NotificationRecord record, boolean autogroupSummaryExists)251 public boolean onNotificationPosted(NotificationRecord record, boolean autogroupSummaryExists) { 252 boolean sbnToBeAutogrouped = false; 253 try { 254 if (notificationForceGrouping()) { 255 final StatusBarNotification sbn = record.getSbn(); 256 if (!sbn.isAppGroup()) { 257 sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists); 258 } else { 259 maybeUngroupOnAppGrouped(record); 260 } 261 } else { 262 final StatusBarNotification sbn = record.getSbn(); 263 if (!sbn.isAppGroup()) { 264 sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists); 265 } else { 266 maybeUngroup(sbn, false, sbn.getUserId()); 267 } 268 } 269 } catch (Exception e) { 270 Slog.e(TAG, "Failure processing new notification", e); 271 } 272 return sbnToBeAutogrouped; 273 } 274 275 /** 276 * Called when a notification was removed. Checks if that notification was part of an autogroup 277 * and triggers any necessary cleanups: summary removal, clearing caches etc. 278 * 279 * @param record The removed notification. 280 */ onNotificationRemoved(NotificationRecord record)281 public void onNotificationRemoved(NotificationRecord record) { 282 try { 283 if (notificationForceGrouping()) { 284 Slog.wtf(TAG, 285 "This overload of onNotificationRemoved() should not be called if " 286 + "notification_force_grouping is enabled!", 287 new Exception("call stack")); 288 onNotificationRemoved(record, new ArrayList<>(), false); 289 } else { 290 final StatusBarNotification sbn = record.getSbn(); 291 maybeUngroup(sbn, true, sbn.getUserId()); 292 } 293 } catch (Exception e) { 294 Slog.e(TAG, "Error processing canceled notification", e); 295 } 296 } 297 298 /** 299 * A non-app grouped notification has been added or updated 300 * Evaluate if: 301 * (a) an existing autogroup summary needs updated flags 302 * (b) a new autogroup summary needs to be added with correct flags 303 * (c) other non-app grouped children need to be moved to the autogroup 304 * 305 * And stores the list of upgrouped notifications & their flags 306 */ maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists)307 private boolean maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) { 308 int flags = 0; 309 List<String> notificationsToGroup = new ArrayList<>(); 310 List<NotificationAttributes> childrenAttr = new ArrayList<>(); 311 // Indicates whether the provided sbn should be autogrouped by the caller. 312 boolean sbnToBeAutogrouped = false; 313 synchronized (mUngroupedNotifications) { 314 String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 315 final ArrayMap<String, NotificationAttributes> children = 316 mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>()); 317 NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags, 318 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 319 sbn.getNotification().visibility, Notification.GROUP_ALERT_CHILDREN, 320 sbn.getNotification().getChannelId()); 321 children.put(sbn.getKey(), attr); 322 mUngroupedNotifications.put(packageKey, children); 323 324 if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) { 325 flags = getAutogroupSummaryFlags(children); 326 notificationsToGroup.addAll(children.keySet()); 327 childrenAttr.addAll(children.values()); 328 } 329 } 330 if (notificationsToGroup.size() > 0) { 331 if (autogroupSummaryExists) { 332 NotificationAttributes attr = new NotificationAttributes(flags, 333 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 334 VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN, 335 sbn.getNotification().getChannelId()); 336 if (Flags.autogroupSummaryIconUpdate()) { 337 attr = updateAutobundledSummaryAttributes(sbn.getPackageName(), childrenAttr, 338 attr); 339 } 340 341 mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), 342 AUTOGROUP_KEY, attr); 343 } else { 344 Icon summaryIcon = sbn.getNotification().getSmallIcon(); 345 int summaryIconColor = sbn.getNotification().color; 346 int summaryVisibility = VISIBILITY_PRIVATE; 347 String summaryChannelId = sbn.getNotification().getChannelId(); 348 if (Flags.autogroupSummaryIconUpdate()) { 349 // Calculate the initial summary icon, icon color and visibility 350 NotificationAttributes iconAttr = getAutobundledSummaryAttributes( 351 sbn.getPackageName(), childrenAttr); 352 summaryIcon = iconAttr.icon; 353 summaryIconColor = iconAttr.iconColor; 354 summaryVisibility = iconAttr.visibility; 355 summaryChannelId = iconAttr.channelId; 356 } 357 358 NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon, 359 summaryIconColor, summaryVisibility, Notification.GROUP_ALERT_CHILDREN, 360 summaryChannelId); 361 mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), 362 AUTOGROUP_KEY, Integer.MAX_VALUE, attr); 363 } 364 for (String keyToGroup : notificationsToGroup) { 365 if (android.app.Flags.checkAutogroupBeforePost()) { 366 if (keyToGroup.equals(sbn.getKey())) { 367 // Autogrouping for the provided notification is to be done synchronously. 368 sbnToBeAutogrouped = true; 369 } else { 370 mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true); 371 } 372 } else { 373 mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true); 374 } 375 } 376 } 377 return sbnToBeAutogrouped; 378 } 379 380 /** 381 * A notification was added that's app grouped, or a notification was removed. 382 * Evaluate whether: 383 * (a) an existing autogroup summary needs updated flags 384 * (b) if we need to remove our autogroup overlay for this notification 385 * (c) we need to remove the autogroup summary 386 * 387 * And updates the internal state of un-app-grouped notifications and their flags. 388 */ maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId)389 private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) { 390 boolean removeSummary = false; 391 int summaryFlags = FLAG_INVALID; 392 boolean updateSummaryFlags = false; 393 boolean removeAutogroupOverlay = false; 394 List<NotificationAttributes> childrenAttrs = new ArrayList<>(); 395 synchronized (mUngroupedNotifications) { 396 String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); 397 final ArrayMap<String, NotificationAttributes> children = 398 mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); 399 if (children.size() == 0) { 400 return; 401 } 402 403 // if this notif was autogrouped and now isn't 404 if (children.containsKey(sbn.getKey())) { 405 // if this notification was contributing flags that aren't covered by other 406 // children to the summary, reevaluate flags for the summary 407 int flags = children.remove(sbn.getKey()).flags; 408 // this 409 if (hasAnyFlag(flags, ANY_CHILDREN_FLAGS)) { 410 updateSummaryFlags = true; 411 summaryFlags = getAutogroupSummaryFlags(children); 412 } 413 // if this notification still exists and has an autogroup overlay, but is now 414 // grouped by the app, clear the overlay 415 if (!notificationGone && sbn.getOverrideGroupKey() != null) { 416 removeAutogroupOverlay = true; 417 } 418 419 // If there are no more children left to autogroup, remove the summary 420 if (children.size() == 0) { 421 removeSummary = true; 422 } else { 423 childrenAttrs.addAll(children.values()); 424 } 425 } 426 } 427 428 if (removeSummary) { 429 mCallback.removeAutoGroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY); 430 } else { 431 NotificationAttributes attr = new NotificationAttributes(summaryFlags, 432 sbn.getNotification().getSmallIcon(), sbn.getNotification().color, 433 VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN, 434 sbn.getNotification().getChannelId()); 435 boolean attributesUpdated = false; 436 if (Flags.autogroupSummaryIconUpdate()) { 437 NotificationAttributes newAttr = updateAutobundledSummaryAttributes( 438 sbn.getPackageName(), childrenAttrs, attr); 439 if (!newAttr.equals(attr)) { 440 attributesUpdated = true; 441 attr = newAttr; 442 } 443 } 444 445 if (updateSummaryFlags || attributesUpdated) { 446 mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY, attr); 447 } 448 } 449 if (removeAutogroupOverlay) { 450 mCallback.removeAutoGroup(sbn.getKey()); 451 } 452 } 453 getAutobundledSummaryAttributes(@onNull String packageName, @NonNull List<NotificationAttributes> childrenAttr)454 NotificationAttributes getAutobundledSummaryAttributes(@NonNull String packageName, 455 @NonNull List<NotificationAttributes> childrenAttr) { 456 Icon newIcon = null; 457 boolean childrenHaveSameIcon = true; 458 int newColor = Notification.COLOR_INVALID; 459 boolean childrenHaveSameColor = true; 460 int newVisibility = VISIBILITY_PRIVATE; 461 462 // Both the icon drawable and the icon background color are updated according to this rule: 463 // - if all child icons are identical => use the common icon 464 // - if child icons are different: use the monochromatic app icon, if exists. 465 // Otherwise fall back to a generic icon representing a stack. 466 for (NotificationAttributes state: childrenAttr) { 467 // Check for icon 468 if (newIcon == null) { 469 newIcon = state.icon; 470 } else { 471 if (!newIcon.sameAs(state.icon)) { 472 childrenHaveSameIcon = false; 473 } 474 } 475 // Check for color 476 if (newColor == Notification.COLOR_INVALID) { 477 newColor = state.iconColor; 478 } else { 479 if (newColor != state.iconColor) { 480 childrenHaveSameColor = false; 481 } 482 } 483 // Check for visibility. If at least one child is public, then set to public 484 if (state.visibility == VISIBILITY_PUBLIC) { 485 newVisibility = VISIBILITY_PUBLIC; 486 } 487 } 488 if (!childrenHaveSameIcon) { 489 newIcon = getMonochromeAppIcon(packageName); 490 } 491 if (!childrenHaveSameColor) { 492 newColor = COLOR_DEFAULT; 493 } 494 495 // Use GROUP_ALERT_CHILDREN 496 // Unless all children have GROUP_ALERT_SUMMARY => avoid muting all notifications in group 497 int newGroupAlertBehavior = Notification.GROUP_ALERT_SUMMARY; 498 for (NotificationAttributes attr: childrenAttr) { 499 if (attr.groupAlertBehavior != Notification.GROUP_ALERT_SUMMARY) { 500 newGroupAlertBehavior = Notification.GROUP_ALERT_CHILDREN; 501 break; 502 } 503 } 504 505 String channelId = !childrenAttr.isEmpty() ? childrenAttr.get(0).channelId : null; 506 507 return new NotificationAttributes(0, newIcon, newColor, newVisibility, 508 newGroupAlertBehavior, channelId); 509 } 510 updateAutobundledSummaryAttributes(@onNull String packageName, @NonNull List<NotificationAttributes> childrenAttr, @NonNull NotificationAttributes oldAttr)511 NotificationAttributes updateAutobundledSummaryAttributes(@NonNull String packageName, 512 @NonNull List<NotificationAttributes> childrenAttr, 513 @NonNull NotificationAttributes oldAttr) { 514 NotificationAttributes newAttr = getAutobundledSummaryAttributes(packageName, 515 childrenAttr); 516 Icon newIcon = newAttr.icon; 517 int newColor = newAttr.iconColor; 518 String newChannelId = newAttr.channelId; 519 if (newAttr.icon == null) { 520 newIcon = oldAttr.icon; 521 } 522 if (newAttr.iconColor == Notification.COLOR_INVALID) { 523 newColor = oldAttr.iconColor; 524 } 525 if (newAttr.channelId == null) { 526 newChannelId = oldAttr.channelId; 527 } 528 529 return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility, 530 oldAttr.groupAlertBehavior, newChannelId); 531 } 532 getSummaryAttributes(String pkgName, ArrayMap<String, NotificationAttributes> childrenMap)533 private NotificationAttributes getSummaryAttributes(String pkgName, 534 ArrayMap<String, NotificationAttributes> childrenMap) { 535 int flags = getAutogroupSummaryFlags(childrenMap); 536 NotificationAttributes attr = getAutobundledSummaryAttributes(pkgName, 537 childrenMap.values().stream().toList()); 538 return new NotificationAttributes(flags, attr.icon, attr.iconColor, attr.visibility, 539 attr.groupAlertBehavior, attr.channelId); 540 } 541 542 /** 543 * Get the monochrome app icon for an app from the adaptive launcher icon 544 * or a fallback generic icon for autogroup summaries. 545 * 546 * @param pkg packageName of the app 547 * @return a monochrome app icon or a fallback generic icon 548 */ 549 @NonNull getMonochromeAppIcon(@onNull final String pkg)550 Icon getMonochromeAppIcon(@NonNull final String pkg) { 551 Icon monochromeIcon = null; 552 final int fallbackIconResId = R.drawable.ic_notification_summary_auto; 553 try { 554 final Drawable appIcon = mPackageManager.getApplicationIcon(pkg); 555 if (appIcon instanceof AdaptiveIconDrawable) { 556 if (((AdaptiveIconDrawable) appIcon).getMonochrome() != null) { 557 monochromeIcon = Icon.createWithResourceAdaptiveDrawable(pkg, 558 ((AdaptiveIconDrawable) appIcon).getSourceDrawableResId(), true, 559 -2.0f * AdaptiveIconDrawable.getExtraInsetFraction()); 560 } 561 } 562 } catch (NameNotFoundException e) { 563 Slog.e(TAG, "Failed to getApplicationIcon() in getMonochromeAppIcon()", e); 564 } 565 if (monochromeIcon != null) { 566 return monochromeIcon; 567 } else { 568 return Icon.createWithResource(mContext, fallbackIconResId); 569 } 570 } 571 572 /** 573 * A non-app-grouped notification has been added or updated 574 * Evaluate if: 575 * (a) an existing autogroup summary needs updated attributes 576 * (b) a new autogroup summary needs to be added with correct attributes 577 * (c) other non-app grouped children need to be moved to the autogroup 578 * (d) the notification has been updated from a groupable to a non-groupable section and needs 579 * to trigger a cleanup 580 * 581 * This method implements autogrouping with sections support. 582 * 583 * And stores the list of upgrouped notifications & their flags 584 */ maybeGroupWithSections(NotificationRecord record, boolean autogroupSummaryExists)585 private boolean maybeGroupWithSections(NotificationRecord record, 586 boolean autogroupSummaryExists) { 587 final StatusBarNotification sbn = record.getSbn(); 588 boolean sbnToBeAutogrouped = false; 589 final NotificationSectioner sectioner = getSection(record); 590 if (sectioner == null) { 591 maybeUngroupOnNonGroupableUpdate(record); 592 if (DEBUG) { 593 Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); 594 } 595 return false; 596 } 597 598 final String pkgName = sbn.getPackageName(); 599 final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( 600 record.getUserId(), pkgName, sectioner); 601 602 // The notification was part of a different section => trigger regrouping 603 final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); 604 if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) { 605 if (DEBUG) { 606 Slog.i(TAG, "Section changed for: " + record); 607 } 608 maybeUngroupOnSectionChanged(record, prevSectionKey); 609 } 610 611 // This notification is already aggregated 612 if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { 613 return false; 614 } 615 synchronized (mAggregatedNotifications) { 616 ArrayMap<String, NotificationAttributes> ungrouped = 617 mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 618 ungrouped.put(record.getKey(), new NotificationAttributes( 619 record.getFlags(), 620 record.getNotification().getSmallIcon(), 621 record.getNotification().color, 622 record.getNotification().visibility, 623 record.getNotification().getGroupAlertBehavior(), 624 record.getChannel().getId())); 625 mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); 626 627 // scenario 0: ungrouped notifications 628 if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) { 629 if (DEBUG) { 630 if (ungrouped.size() >= mAutoGroupAtCount) { 631 Slog.i(TAG, 632 "Found >=" + mAutoGroupAtCount 633 + " ungrouped notifications => force grouping"); 634 } else { 635 Slog.i(TAG, "Found aggregate summary => force grouping"); 636 } 637 } 638 639 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 640 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 641 aggregatedNotificationsAttrs.putAll(ungrouped); 642 mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); 643 644 // add/update aggregate summary 645 updateAggregateAppGroup(fullAggregateGroupKey, record.getKey(), 646 autogroupSummaryExists, sectioner.mSummaryId); 647 648 // add notification to aggregate group 649 for (String keyToGroup : ungrouped.keySet()) { 650 if (android.app.Flags.checkAutogroupBeforePost()) { 651 if (keyToGroup.equals(record.getKey())) { 652 // Autogrouping for the posted notification is to be done synchronously. 653 sbnToBeAutogrouped = true; 654 } else { 655 mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(), 656 true); 657 } 658 } else { 659 mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(), true); 660 } 661 } 662 663 //cleanup mUngroupedAbuseNotifications 664 mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); 665 } 666 } 667 668 return sbnToBeAutogrouped; 669 } 670 671 /** 672 * A notification was added that was previously part of a valid section and needs to trigger 673 * GH state cleanup. 674 */ maybeUngroupOnNonGroupableUpdate(NotificationRecord record)675 private void maybeUngroupOnNonGroupableUpdate(NotificationRecord record) { 676 maybeUngroupWithSections(record, getPreviousValidSectionKey(record)); 677 } 678 679 /** 680 * A notification was added that was previously part of a different section and needs to trigger 681 * GH state cleanup. 682 */ maybeUngroupOnSectionChanged(NotificationRecord record, FullyQualifiedGroupKey prevSectionKey)683 private void maybeUngroupOnSectionChanged(NotificationRecord record, 684 FullyQualifiedGroupKey prevSectionKey) { 685 maybeUngroupWithSections(record, prevSectionKey); 686 if (record.getGroupKey().equals(prevSectionKey.toString())) { 687 record.setOverrideGroupKey(null); 688 } 689 } 690 691 /** 692 * A notification was added that is app-grouped. 693 */ maybeUngroupOnAppGrouped(NotificationRecord record)694 private void maybeUngroupOnAppGrouped(NotificationRecord record) { 695 FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record); 696 697 // The notification was part of a different section => trigger regrouping 698 final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); 699 if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) { 700 if (DEBUG) { 701 Slog.i(TAG, "Section changed for: " + record); 702 } 703 currentSectionKey = prevSectionKey; 704 } 705 706 maybeUngroupWithSections(record, currentSectionKey); 707 } 708 709 /** 710 * Called when a notification is posted and is either app-grouped or was previously part of 711 * a valid section and needs to trigger GH state cleanup. 712 * 713 * Evaluate whether: 714 * (a) an existing autogroup summary needs updated attributes 715 * (b) if we need to remove our autogroup overlay for this notification 716 * (c) we need to remove the autogroup summary 717 * 718 * This method implements autogrouping with sections support. 719 * 720 * And updates the internal state of un-app-grouped notifications and their flags. 721 */ maybeUngroupWithSections(NotificationRecord record, @Nullable FullyQualifiedGroupKey fullAggregateGroupKey)722 private void maybeUngroupWithSections(NotificationRecord record, 723 @Nullable FullyQualifiedGroupKey fullAggregateGroupKey) { 724 if (fullAggregateGroupKey == null) { 725 if (DEBUG) { 726 Slog.i(TAG, 727 "Skipping maybeUngroupWithSections for " + record 728 + " no valid section found."); 729 } 730 return; 731 } 732 733 final StatusBarNotification sbn = record.getSbn(); 734 final String pkgName = sbn.getPackageName(); 735 final int userId = record.getUserId(); 736 synchronized (mAggregatedNotifications) { 737 // if this notification still exists and has an autogroup overlay, but is now 738 // grouped by the app, clear the overlay 739 ArrayMap<String, NotificationAttributes> ungrouped = 740 mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 741 ungrouped.remove(sbn.getKey()); 742 mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); 743 744 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 745 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 746 // check if the removed notification was part of the aggregate group 747 if (aggregatedNotificationsAttrs.containsKey(record.getKey())) { 748 aggregatedNotificationsAttrs.remove(sbn.getKey()); 749 mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); 750 751 if (DEBUG) { 752 Slog.i(TAG, "maybeUngroup removeAutoGroup: " + record); 753 } 754 755 mCallback.removeAutoGroup(sbn.getKey()); 756 757 if (aggregatedNotificationsAttrs.isEmpty()) { 758 if (DEBUG) { 759 Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey); 760 } 761 mCallback.removeAutoGroupSummary(userId, pkgName, 762 fullAggregateGroupKey.toString()); 763 mAggregatedNotifications.remove(fullAggregateGroupKey); 764 } else { 765 if (DEBUG) { 766 Slog.i(TAG, 767 "Aggregate group not empty, updating: " + fullAggregateGroupKey); 768 } 769 updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0); 770 } 771 } 772 } 773 } 774 775 /** 776 * Called when a notification is newly posted, after some delay, so that the app 777 * has a chance to post a group summary or children (complete a group). 778 * Checks whether that notification and other active notifications should be forced grouped 779 * because their grouping is incorrect: 780 * - missing summary 781 * - only summaries 782 * - sparse groups == multiple groups with very few notifications 783 * 784 * @param record the notification that was posted 785 * @param notificationList the full notification list from NotificationManagerService 786 * @param summaryByGroupKey the map of group summaries from NotificationManagerService 787 */ 788 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onNotificationPostedWithDelay(final NotificationRecord record, final List<NotificationRecord> notificationList, final Map<String, NotificationRecord> summaryByGroupKey)789 protected void onNotificationPostedWithDelay(final NotificationRecord record, 790 final List<NotificationRecord> notificationList, 791 final Map<String, NotificationRecord> summaryByGroupKey) { 792 // Ungrouped notifications are handled separately in 793 // {@link #onNotificationPosted(StatusBarNotification, boolean)} 794 final StatusBarNotification sbn = record.getSbn(); 795 if (!sbn.isAppGroup()) { 796 return; 797 } 798 799 if (record.isCanceled) { 800 return; 801 } 802 803 if (mIsTestHarnessExempted) { 804 return; 805 } 806 807 final NotificationSectioner sectioner = getSection(record); 808 if (sectioner == null) { 809 if (DEBUG) { 810 Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); 811 } 812 return; 813 } 814 815 final String pkgName = sbn.getPackageName(); 816 final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( 817 record.getUserId(), pkgName, sectioner); 818 819 // This notification is already aggregated 820 if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { 821 return; 822 } 823 824 synchronized (mAggregatedNotifications) { 825 // scenario 1: group w/o summary 826 // scenario 2: summary w/o children 827 if (isGroupChildWithoutSummary(record, summaryByGroupKey) || 828 isGroupSummaryWithoutChildren(record, notificationList)) { 829 if (DEBUG) { 830 Log.i(TAG, "isGroupChildWithoutSummary OR isGroupSummaryWithoutChild" 831 + record); 832 } 833 addToUngroupedAndMaybeAggregate(record, fullAggregateGroupKey, sectioner); 834 return; 835 } 836 837 // Check if summary & child notifications are not part of the same section/bundle 838 // Needs a check here if notification was bundled while enqueued 839 if (notificationRegroupOnClassification() 840 && android.service.notification.Flags.notificationClassification()) { 841 if (isGroupChildBundled(record, summaryByGroupKey)) { 842 if (DEBUG) { 843 Slog.v(TAG, "isGroupChildInDifferentBundleThanSummary: " + record); 844 } 845 moveNotificationsToNewSection(record.getUserId(), pkgName, 846 List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)), 847 Map.of(record.getKey(), REGROUP_REASON_BUNDLE)); 848 return; 849 } 850 } 851 852 // scenario 3: sparse/singleton groups 853 if (Flags.notificationForceGroupSingletons()) { 854 try { 855 groupSparseGroups(record, notificationList, summaryByGroupKey, sectioner, 856 fullAggregateGroupKey); 857 } catch (Throwable e) { 858 Slog.wtf(TAG, "Failed to group sparse groups", e); 859 } 860 } 861 } 862 } 863 864 @GuardedBy("mAggregatedNotifications") addToUngroupedAndMaybeAggregate(NotificationRecord record, FullyQualifiedGroupKey fullAggregateGroupKey, NotificationSectioner sectioner)865 private void addToUngroupedAndMaybeAggregate(NotificationRecord record, 866 FullyQualifiedGroupKey fullAggregateGroupKey, NotificationSectioner sectioner) { 867 ArrayMap<String, NotificationAttributes> ungrouped = 868 mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, 869 new ArrayMap<>()); 870 ungrouped.put(record.getKey(), new NotificationAttributes( 871 record.getFlags(), 872 record.getNotification().getSmallIcon(), 873 record.getNotification().color, 874 record.getNotification().visibility, 875 record.getNotification().getGroupAlertBehavior(), 876 record.getChannel().getId())); 877 mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); 878 // Create/update summary and group if >= mAutoGroupAtCount notifications 879 // or if aggregate group exists 880 boolean hasSummary = !mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, 881 new ArrayMap<>()).isEmpty(); 882 if (ungrouped.size() >= mAutoGroupAtCount || hasSummary) { 883 if (DEBUG) { 884 if (ungrouped.size() >= mAutoGroupAtCount) { 885 Slog.i(TAG, "Found >=" + mAutoGroupAtCount 886 + " ungrouped notifications => force grouping"); 887 } else { 888 Slog.i(TAG, "Found aggregate summary => force grouping"); 889 } 890 } 891 aggregateUngroupedNotifications(fullAggregateGroupKey, record.getKey(), 892 ungrouped, hasSummary, sectioner.mSummaryId); 893 } 894 } 895 isGroupChildBundled(final NotificationRecord record, final Map<String, NotificationRecord> summaryByGroupKey)896 private static boolean isGroupChildBundled(final NotificationRecord record, 897 final Map<String, NotificationRecord> summaryByGroupKey) { 898 final StatusBarNotification sbn = record.getSbn(); 899 final String groupKey = record.getSbn().getGroupKey(); 900 901 if (!sbn.isAppGroup()) { 902 return false; 903 } 904 905 if (record.getNotification().isGroupSummary()) { 906 return false; 907 } 908 909 final NotificationRecord summary = summaryByGroupKey.get(groupKey); 910 if (summary == null) { 911 return false; 912 } 913 914 return isInBundleSection(record); 915 } 916 isInBundleSection(final NotificationRecord record)917 private static boolean isInBundleSection(final NotificationRecord record) { 918 final NotificationSectioner sectioner = getSection(record); 919 return (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner)); 920 } 921 922 /** 923 * Called when a notification is removed, so that this helper can adjust the aggregate groups: 924 * - Removes the autogroup summary of the notification's section 925 * if the record was the last child. 926 * - Recalculates the autogroup summary "attributes": 927 * icon, icon color, visibility, groupAlertBehavior, flags - if the removed record was 928 * part of an autogroup. 929 * - Removes the saved summary of the original group, if the record was the last remaining 930 * child of a sparse group that was forced auto-grouped. 931 * 932 * see also {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} 933 * 934 * @param record the removed notification 935 * @param notificationList the full notification list from NotificationManagerService 936 * @param sendingDelete whether the removed notification is being removed in a way that sends 937 * its {@code deleteIntent} 938 */ 939 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onNotificationRemoved(final NotificationRecord record, final List<NotificationRecord> notificationList, boolean sendingDelete)940 protected void onNotificationRemoved(final NotificationRecord record, 941 final List<NotificationRecord> notificationList, boolean sendingDelete) { 942 final StatusBarNotification sbn = record.getSbn(); 943 final String pkgName = sbn.getPackageName(); 944 final int userId = record.getUserId(); 945 946 final FullyQualifiedGroupKey fullAggregateGroupKey = getSectionGroupKeyWithFallback(record); 947 if (fullAggregateGroupKey == null) { 948 if (DEBUG) { 949 Slog.i(TAG, 950 "Skipping autogroup cleanup for " + record + " no valid section found."); 951 } 952 return; 953 } 954 955 synchronized (mAggregatedNotifications) { 956 ArrayMap<String, NotificationAttributes> ungrouped = 957 mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 958 ungrouped.remove(record.getKey()); 959 mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); 960 961 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 962 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 963 // check if the removed notification was part of the aggregate group 964 if (record.getGroupKey().equals(fullAggregateGroupKey.toString()) 965 || aggregatedNotificationsAttrs.containsKey(record.getKey())) { 966 aggregatedNotificationsAttrs.remove(record.getKey()); 967 mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); 968 969 if (aggregatedNotificationsAttrs.isEmpty()) { 970 if (DEBUG) { 971 Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey); 972 } 973 mCallback.removeAutoGroupSummary(userId, pkgName, 974 fullAggregateGroupKey.toString()); 975 mAggregatedNotifications.remove(fullAggregateGroupKey); 976 } else { 977 if (DEBUG) { 978 Slog.i(TAG, 979 "Aggregate group not empty, updating: " + fullAggregateGroupKey); 980 } 981 updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0); 982 } 983 984 // Try to cleanup cached summaries if notification was canceled (not snoozed) 985 // If the notification was cancelled by an action that fires its delete intent, 986 // also fire it for the cached summary. 987 if (record.isCanceled) { 988 maybeClearCanceledSummariesCache(pkgName, userId, 989 record.getNotification().getGroup(), notificationList, sendingDelete); 990 } 991 } 992 } 993 } 994 995 /** 996 * Get the section key for a notification. If the section is invalid, ie. notification is not 997 * auto-groupable, then return the previous valid section, if any. 998 * @param record the notification 999 * @return a section group key, null if not found 1000 */ 1001 @Nullable getSectionGroupKeyWithFallback(final NotificationRecord record)1002 private FullyQualifiedGroupKey getSectionGroupKeyWithFallback(final NotificationRecord record) { 1003 final NotificationSectioner sectioner = getSection(record); 1004 if (sectioner != null) { 1005 return FullyQualifiedGroupKey.forRecord(record, sectioner); 1006 } else { 1007 return getPreviousValidSectionKey(record); 1008 } 1009 } 1010 1011 /** 1012 * Get the previous valid section key of a notification that may have been updated to an invalid 1013 * section. This is needed in case a notification is updated as an ungroupable (invalid section) 1014 * => auto-groups need to be updated/GH state cleanup. 1015 * @param record the notification 1016 * @return a section group key or null if not found 1017 */ 1018 @Nullable getPreviousValidSectionKey(final NotificationRecord record)1019 private FullyQualifiedGroupKey getPreviousValidSectionKey(final NotificationRecord record) { 1020 synchronized (mAggregatedNotifications) { 1021 final String recordKey = record.getKey(); 1022 // Search in ungrouped 1023 for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> 1024 ungroupedSection : mUngroupedAbuseNotifications.entrySet()) { 1025 if (ungroupedSection.getValue().containsKey(recordKey)) { 1026 return ungroupedSection.getKey(); 1027 } 1028 } 1029 // Search in aggregated 1030 for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> 1031 aggregatedSection : mAggregatedNotifications.entrySet()) { 1032 if (aggregatedSection.getValue().containsKey(recordKey)) { 1033 return aggregatedSection.getKey(); 1034 } 1035 } 1036 } 1037 return null; 1038 } 1039 1040 /** 1041 * Called when a child notification is removed, after some delay, so that this helper can 1042 * trigger a forced grouping if the group has become sparse/singleton 1043 * or only the summary is left. 1044 * 1045 * see also {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} 1046 * 1047 * @param summaryRecord the group summary of the notification that was removed 1048 * @param notificationList the full notification list from NotificationManagerService 1049 * @param summaryByGroupKey the map of group summaries from NotificationManagerService 1050 */ 1051 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onGroupedNotificationRemovedWithDelay(final NotificationRecord summaryRecord, final List<NotificationRecord> notificationList, final Map<String, NotificationRecord> summaryByGroupKey)1052 protected void onGroupedNotificationRemovedWithDelay(final NotificationRecord summaryRecord, 1053 final List<NotificationRecord> notificationList, 1054 final Map<String, NotificationRecord> summaryByGroupKey) { 1055 final StatusBarNotification sbn = summaryRecord.getSbn(); 1056 if (!sbn.isAppGroup()) { 1057 return; 1058 } 1059 1060 if (summaryRecord.isCanceled) { 1061 return; 1062 } 1063 1064 if (mIsTestHarnessExempted) { 1065 return; 1066 } 1067 1068 final NotificationSectioner sectioner = getSection(summaryRecord); 1069 if (sectioner == null) { 1070 if (DEBUG) { 1071 Slog.i(TAG, 1072 "Skipping autogrouping for " + summaryRecord + " no valid section found."); 1073 } 1074 return; 1075 } 1076 1077 final String pkgName = sbn.getPackageName(); 1078 final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( 1079 summaryRecord.getUserId(), pkgName, sectioner); 1080 1081 // This notification is already aggregated 1082 if (summaryRecord.getGroupKey().equals(fullAggregateGroupKey.toString())) { 1083 return; 1084 } 1085 1086 synchronized (mAggregatedNotifications) { 1087 if (isGroupSummaryWithoutChildren(summaryRecord, notificationList)) { 1088 if (DEBUG) { 1089 Slog.i(TAG, "isGroupSummaryWithoutChild " + summaryRecord); 1090 } 1091 addToUngroupedAndMaybeAggregate(summaryRecord, fullAggregateGroupKey, sectioner); 1092 return; 1093 } 1094 1095 // Check if notification removal turned this group into a sparse/singleton group 1096 if (Flags.notificationForceGroupSingletons()) { 1097 try { 1098 groupSparseGroups(summaryRecord, notificationList, summaryByGroupKey, sectioner, 1099 fullAggregateGroupKey); 1100 } catch (Throwable e) { 1101 Slog.wtf(TAG, "Failed to group sparse groups", e); 1102 } 1103 } 1104 } 1105 } 1106 1107 /** 1108 * Called when a group summary is posted. If there are any ungrouped notifications that are 1109 * in that group, remove them as they are no longer candidates for autogrouping. 1110 * 1111 * @param summaryRecord the NotificationRecord for the newly posted group summary 1112 * @param notificationList the full notification list from NotificationManagerService 1113 */ 1114 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onGroupSummaryAdded(final NotificationRecord summaryRecord, final List<NotificationRecord> notificationList)1115 protected void onGroupSummaryAdded(final NotificationRecord summaryRecord, 1116 final List<NotificationRecord> notificationList) { 1117 String groupKey = summaryRecord.getSbn().getGroup(); 1118 synchronized (mAggregatedNotifications) { 1119 final NotificationSectioner sectioner = getSection(summaryRecord); 1120 if (sectioner == null) { 1121 Slog.w(TAG, "onGroupSummaryAdded " + summaryRecord + ": no valid section found"); 1122 return; 1123 } 1124 1125 FullyQualifiedGroupKey aggregateGroupKey = FullyQualifiedGroupKey.forRecord( 1126 summaryRecord, sectioner); 1127 ArrayMap<String, NotificationAttributes> ungrouped = 1128 mUngroupedAbuseNotifications.getOrDefault(aggregateGroupKey, 1129 new ArrayMap<>()); 1130 if (ungrouped.isEmpty()) { 1131 // don't bother looking through the notification list if there are no pending 1132 // ungrouped notifications in this section (likely to be the most common case) 1133 return; 1134 } 1135 1136 // Look through full notification list for any notifications belonging to this group; 1137 // remove from ungrouped map if needed, as the presence of the summary means they will 1138 // now be grouped 1139 for (NotificationRecord r : notificationList) { 1140 if (!r.getNotification().isGroupSummary() 1141 && groupKey.equals(r.getSbn().getGroup()) 1142 && ungrouped.containsKey(r.getKey())) { 1143 ungrouped.remove(r.getKey()); 1144 } 1145 } 1146 mUngroupedAbuseNotifications.put(aggregateGroupKey, ungrouped); 1147 } 1148 } 1149 NotificationMoveOp(NotificationRecord record, FullyQualifiedGroupKey oldGroup, FullyQualifiedGroupKey newGroup)1150 private record NotificationMoveOp(NotificationRecord record, FullyQualifiedGroupKey oldGroup, 1151 FullyQualifiedGroupKey newGroup) { } 1152 1153 /** 1154 * Called when a notification channel is updated (channel attributes have changed), so that this 1155 * helper can adjust the aggregate groups by moving children if their section has changed. see 1156 * {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} 1157 * 1158 * @param userId the userId of the channel 1159 * @param pkgName the channel's package 1160 * @param channel the channel that was updated 1161 * @param notificationList the full notification list from NotificationManagerService 1162 */ 1163 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onChannelUpdated(final int userId, final String pkgName, final NotificationChannel channel, final List<NotificationRecord> notificationList, ArrayMap<String, NotificationRecord> summaryByGroupKey)1164 public void onChannelUpdated(final int userId, final String pkgName, 1165 final NotificationChannel channel, final List<NotificationRecord> notificationList, 1166 ArrayMap<String, NotificationRecord> summaryByGroupKey) { 1167 synchronized (mAggregatedNotifications) { 1168 final ArrayMap<String, Integer> regroupingReasonMap = new ArrayMap<>(); 1169 ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); 1170 for (NotificationRecord r : notificationList) { 1171 if (r.getChannel().getId().equals(channel.getId()) 1172 && r.getSbn().getPackageName().equals(pkgName) 1173 && r.getUserId() == userId) { 1174 notificationsToCheck.put(r.getKey(), r); 1175 regroupingReasonMap.put(r.getKey(), REGROUP_REASON_CHANNEL_UPDATE); 1176 if (notificationRegroupOnClassification()) { 1177 // Notification is unbundled and original summary found 1178 // => regroup in original group 1179 if (!isInBundleSection(r) 1180 && isOriginalGroupSummaryPresent(r, summaryByGroupKey)) { 1181 regroupingReasonMap.put(r.getKey(), 1182 REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP); 1183 } 1184 } 1185 } 1186 } 1187 1188 regroupNotifications(userId, pkgName, notificationsToCheck, regroupingReasonMap); 1189 } 1190 } 1191 1192 /** 1193 * Called when an individuial notification's channel is updated (moved to a new channel), 1194 * so that this helper can adjust the aggregate groups by moving children 1195 * if their section has changed. 1196 * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} 1197 * 1198 * @param record the notification which had its channel updated 1199 */ 1200 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onChannelUpdated(final NotificationRecord record)1201 public void onChannelUpdated(final NotificationRecord record) { 1202 synchronized (mAggregatedNotifications) { 1203 ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); 1204 notificationsToCheck.put(record.getKey(), record); 1205 ArrayMap<String, Integer> regroupReasons = new ArrayMap<>(); 1206 regroupReasons.put(record.getKey(), REGROUP_REASON_BUNDLE); 1207 regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), 1208 notificationsToCheck, regroupReasons); 1209 } 1210 } 1211 1212 /** 1213 * Called when a notification that was classified (bundled) is restored to its original channel. 1214 * The notification will be restored to its original group, if any/if summary still exists. 1215 * Otherwise it will be moved to the appropriate section as an ungrouped notification. 1216 * 1217 * @param record the notification which had its channel updated 1218 * @param originalSummaryExists the original group summary exists 1219 */ 1220 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) onNotificationUnbundled(final NotificationRecord record, final boolean originalSummaryExists)1221 public void onNotificationUnbundled(final NotificationRecord record, 1222 final boolean originalSummaryExists) { 1223 synchronized (mAggregatedNotifications) { 1224 ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); 1225 notificationsToCheck.put(record.getKey(), record); 1226 regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), 1227 notificationsToCheck, Map.of(record.getKey(), 1228 originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP 1229 : REGROUP_REASON_UNBUNDLE)); 1230 } 1231 } 1232 1233 @GuardedBy("mAggregatedNotifications") regroupNotifications(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck, Map<String, Integer> regroupReasons)1234 private void regroupNotifications(int userId, String pkgName, 1235 ArrayMap<String, NotificationRecord> notificationsToCheck, 1236 Map<String, Integer> regroupReasons) { 1237 // The list of notification operations required after the channel update 1238 final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); 1239 1240 // Check any already auto-grouped notifications that may need to be re-grouped 1241 // after the channel update 1242 notificationsToMove.addAll( 1243 getAutogroupedNotificationsMoveOps(userId, pkgName, 1244 notificationsToCheck)); 1245 1246 // Check any ungrouped notifications that may need to be auto-grouped 1247 // after the channel update 1248 notificationsToMove.addAll( 1249 getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); 1250 1251 // Handle "grouped correctly" notifications that were re-classified (bundled) 1252 if (notificationRegroupOnClassification()) { 1253 notificationsToMove.addAll( 1254 getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); 1255 } 1256 1257 // Batch move to new section 1258 if (!notificationsToMove.isEmpty()) { 1259 moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupReasons); 1260 } 1261 } 1262 getReclassifiedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck)1263 private List<NotificationMoveOp> getReclassifiedNotificationsMoveOps(int userId, 1264 String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { 1265 final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); 1266 for (NotificationRecord record : notificationsToCheck.values()) { 1267 if (isChildOfValidAppGroup(record)) { 1268 // Check if section changes to a bundle section 1269 NotificationSectioner sectioner = getSection(record); 1270 if (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner)) { 1271 FullyQualifiedGroupKey newFullAggregateGroupKey = 1272 new FullyQualifiedGroupKey(userId, pkgName, sectioner); 1273 if (DEBUG) { 1274 Slog.v(TAG, "Regroup after classification: " + record + " to: " 1275 + newFullAggregateGroupKey); 1276 } 1277 notificationsToMove.add( 1278 new NotificationMoveOp(record, null, newFullAggregateGroupKey)); 1279 } 1280 } 1281 } 1282 return notificationsToMove; 1283 } 1284 1285 /** 1286 * Checks if the original group's summary exists for a notification that was regrouped 1287 * @param r notification to check 1288 * @param summaryByGroupKey map of the current group summaries 1289 * @return true if the original group summary exists 1290 */ isOriginalGroupSummaryPresent(final NotificationRecord r, final ArrayMap<String, NotificationRecord> summaryByGroupKey)1291 public static boolean isOriginalGroupSummaryPresent(final NotificationRecord r, 1292 final ArrayMap<String, NotificationRecord> summaryByGroupKey) { 1293 if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { 1294 final String oldGroupKey = GroupHelper.getFullAggregateGroupKey( 1295 r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId()); 1296 NotificationRecord groupSummary = summaryByGroupKey.get(oldGroupKey); 1297 // We only care about app-provided valid groups 1298 return (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary)); 1299 } 1300 return false; 1301 } 1302 1303 @GuardedBy("mAggregatedNotifications") getAutogroupedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck)1304 private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName, 1305 ArrayMap<String, NotificationRecord> notificationsToCheck) { 1306 final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); 1307 final Set<FullyQualifiedGroupKey> oldGroups = 1308 new HashSet<>(mAggregatedNotifications.keySet()); 1309 // Move auto-grouped updated notifications from the old groups to the new groups (section) 1310 for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) { 1311 // Only check aggregate groups that match the same userId & packageName 1312 if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) { 1313 final ArrayMap<String, NotificationAttributes> notificationsInAggGroup = 1314 mAggregatedNotifications.get(oldFullAggKey); 1315 if (notificationsInAggGroup == null) { 1316 continue; 1317 } 1318 1319 FullyQualifiedGroupKey newFullAggregateGroupKey = null; 1320 for (String key : notificationsInAggGroup.keySet()) { 1321 if (notificationsToCheck.get(key) != null) { 1322 // check if section changes 1323 NotificationSectioner sectioner = getSection(notificationsToCheck.get(key)); 1324 if (sectioner == null) { 1325 continue; 1326 } 1327 newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, 1328 sectioner); 1329 if (!oldFullAggKey.equals(newFullAggregateGroupKey)) { 1330 if (DEBUG) { 1331 Log.i(TAG, "Change section on channel update: " + key); 1332 } 1333 notificationsToMove.add( 1334 new NotificationMoveOp(notificationsToCheck.get(key), 1335 oldFullAggKey, newFullAggregateGroupKey)); 1336 notificationsToCheck.remove(key); 1337 } 1338 } 1339 } 1340 } 1341 } 1342 return notificationsToMove; 1343 } 1344 1345 @GuardedBy("mAggregatedNotifications") getUngroupedNotificationsMoveOps(int userId, String pkgName, final ArrayMap<String, NotificationRecord> notificationsToCheck)1346 private List<NotificationMoveOp> getUngroupedNotificationsMoveOps(int userId, String pkgName, 1347 final ArrayMap<String, NotificationRecord> notificationsToCheck) { 1348 final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); 1349 // Move any remaining ungrouped updated notifications from the old ungrouped list 1350 // to the new ungrouped section list, if necessary 1351 if (!notificationsToCheck.isEmpty()) { 1352 final Set<FullyQualifiedGroupKey> oldUngroupedSectionKeys = 1353 new HashSet<>(mUngroupedAbuseNotifications.keySet()); 1354 for (FullyQualifiedGroupKey oldFullAggKey : oldUngroupedSectionKeys) { 1355 // Only check aggregate groups that match the same userId & packageName 1356 if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) { 1357 final ArrayMap<String, NotificationAttributes> ungroupedOld = 1358 mUngroupedAbuseNotifications.get(oldFullAggKey); 1359 if (ungroupedOld == null) { 1360 continue; 1361 } 1362 1363 FullyQualifiedGroupKey newFullAggregateGroupKey = null; 1364 final Set<String> ungroupedKeys = new HashSet<>(ungroupedOld.keySet()); 1365 for (String key : ungroupedKeys) { 1366 NotificationRecord record = notificationsToCheck.get(key); 1367 if (record != null) { 1368 // check if section changes 1369 NotificationSectioner sectioner = getSection(record); 1370 if (sectioner == null) { 1371 continue; 1372 } 1373 newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, 1374 sectioner); 1375 if (!oldFullAggKey.equals(newFullAggregateGroupKey)) { 1376 if (DEBUG) { 1377 Log.i(TAG, "Change ungrouped section: " + key); 1378 } 1379 notificationsToMove.add( 1380 new NotificationMoveOp(record, oldFullAggKey, 1381 newFullAggregateGroupKey)); 1382 notificationsToCheck.remove(key); 1383 //Remove from previous ungrouped list 1384 ungroupedOld.remove(key); 1385 } 1386 } 1387 } 1388 mUngroupedAbuseNotifications.put(oldFullAggKey, ungroupedOld); 1389 } 1390 } 1391 } 1392 return notificationsToMove; 1393 } 1394 1395 @GuardedBy("mAggregatedNotifications") moveNotificationsToNewSection(final int userId, final String pkgName, final List<NotificationMoveOp> notificationsToMove, final Map<String, Integer> regroupReasons)1396 private void moveNotificationsToNewSection(final int userId, final String pkgName, 1397 final List<NotificationMoveOp> notificationsToMove, 1398 final Map<String, Integer> regroupReasons) { 1399 record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record, 1400 boolean hasSummary) { } 1401 // Bundled operations to apply to groups affected by the channel update 1402 ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>(); 1403 1404 // App-provided (valid) groups of notifications that were classified (bundled). 1405 // Summaries will be canceled if all child notifications have been bundled. 1406 ArrayMap<String, String> originalGroupsOfBundledNotifications = new ArrayMap<>(); 1407 1408 for (NotificationMoveOp moveOp: notificationsToMove) { 1409 final NotificationRecord record = moveOp.record; 1410 final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup; 1411 final FullyQualifiedGroupKey newFullAggregateGroupKey = moveOp.newGroup; 1412 1413 if (DEBUG) { 1414 Log.i(TAG, 1415 "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey 1416 + " from: " + oldFullAggregateGroupKey + " regroupingReason: " 1417 + regroupReasons); 1418 } 1419 1420 // Update/remove aggregate summary for old group 1421 if (oldFullAggregateGroupKey != null) { 1422 final ArrayMap<String, NotificationAttributes> oldAggregatedNotificationsAttrs = 1423 mAggregatedNotifications.getOrDefault(oldFullAggregateGroupKey, 1424 new ArrayMap<>()); 1425 oldAggregatedNotificationsAttrs.remove(record.getKey()); 1426 mAggregatedNotifications.put(oldFullAggregateGroupKey, 1427 oldAggregatedNotificationsAttrs); 1428 1429 // Only add once, for triggering notification 1430 if (!groupsToUpdate.containsKey(oldFullAggregateGroupKey)) { 1431 groupsToUpdate.put(oldFullAggregateGroupKey, 1432 new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); 1433 } 1434 } else { 1435 if (notificationRegroupOnClassification()) { 1436 // Null "old aggregate group" => this notification was re-classified from 1437 // a valid app-provided group => maybe cancel the original summary 1438 // if no children are left 1439 originalGroupsOfBundledNotifications.put(record.getKey(), record.getGroupKey()); 1440 } 1441 } 1442 1443 // Add moved notifications to the ungrouped list for new group and do grouping 1444 // after all notifications have been handled 1445 if (newFullAggregateGroupKey != null) { 1446 if (notificationRegroupOnClassification() 1447 && regroupReasons.getOrDefault(record.getKey(), REGROUP_REASON_CHANNEL_UPDATE) 1448 == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) { 1449 // Just reset override group key, original summary exists 1450 // => will be grouped back to its original group 1451 record.setOverrideGroupKey(null); 1452 } else { 1453 final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs = 1454 mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey, 1455 new ArrayMap<>()); 1456 boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty(); 1457 ArrayMap<String, NotificationAttributes> ungrouped = 1458 mUngroupedAbuseNotifications.getOrDefault(newFullAggregateGroupKey, 1459 new ArrayMap<>()); 1460 ungrouped.put(record.getKey(), new NotificationAttributes( 1461 record.getFlags(), 1462 record.getNotification().getSmallIcon(), 1463 record.getNotification().color, 1464 record.getNotification().visibility, 1465 record.getNotification().getGroupAlertBehavior(), 1466 record.getChannel().getId())); 1467 mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped); 1468 1469 record.setOverrideGroupKey(null); 1470 1471 // Only add once, for triggering notification 1472 if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) { 1473 groupsToUpdate.put(newFullAggregateGroupKey, 1474 new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary)); 1475 } 1476 } 1477 } 1478 } 1479 1480 // Update groups (sections) 1481 for (FullyQualifiedGroupKey groupKey : groupsToUpdate.keySet()) { 1482 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 1483 mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()); 1484 final ArrayMap<String, NotificationAttributes> ungrouped = 1485 mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>()); 1486 1487 NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; 1488 boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; 1489 //Group needs to be created/updated 1490 if (ungrouped.size() >= mAutoGroupAtCount 1491 || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) { 1492 NotificationSectioner sectioner = getSection(triggeringNotification); 1493 if (sectioner == null) { 1494 continue; 1495 } 1496 aggregateUngroupedNotifications(groupKey, triggeringNotification.getKey(), 1497 ungrouped, hasSummary, sectioner.mSummaryId); 1498 } else { 1499 // Remove empty groups 1500 if (aggregatedNotificationsAttrs.isEmpty() && hasSummary) { 1501 mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString()); 1502 mAggregatedNotifications.remove(groupKey); 1503 } 1504 } 1505 } 1506 1507 if (notificationRegroupOnClassification()) { 1508 // Cancel the summary if it's the last notification of the original app-provided group 1509 for (String triggeringKey : originalGroupsOfBundledNotifications.keySet()) { 1510 NotificationRecord canceledSummary = 1511 mCallback.removeAppProvidedSummaryOnClassification(triggeringKey, 1512 originalGroupsOfBundledNotifications.getOrDefault(triggeringKey, null)); 1513 if (canceledSummary != null) { 1514 cacheCanceledSummary(canceledSummary); 1515 } 1516 } 1517 } 1518 } 1519 getFullAggregateGroupKey(String pkgName, String groupName, int userId)1520 static String getFullAggregateGroupKey(String pkgName, 1521 String groupName, int userId) { 1522 return new FullyQualifiedGroupKey(userId, pkgName, groupName).toString(); 1523 } 1524 1525 /** 1526 * Returns the full aggregate group key, which contains the userId and package name 1527 * in addition to the aggregate group key (name). 1528 * Equivalent to {@link StatusBarNotification#groupKey()} 1529 */ getFullAggregateGroupKey(NotificationRecord record)1530 static String getFullAggregateGroupKey(NotificationRecord record) { 1531 return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(), 1532 getSection(record)).toString(); 1533 } 1534 isAggregatedGroup(NotificationRecord record)1535 protected static boolean isAggregatedGroup(NotificationRecord record) { 1536 return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0; 1537 } 1538 isNotificationAggregatedInSection(NotificationRecord record, NotificationSectioner sectioner)1539 private boolean isNotificationAggregatedInSection(NotificationRecord record, 1540 NotificationSectioner sectioner) { 1541 final FullyQualifiedGroupKey fullAggregateGroupKey = FullyQualifiedGroupKey.forRecord( 1542 record, sectioner); 1543 return record.getGroupKey().equals(fullAggregateGroupKey.toString()); 1544 } 1545 isChildOfValidAppGroup(NotificationRecord record)1546 private boolean isChildOfValidAppGroup(NotificationRecord record) { 1547 final StatusBarNotification sbn = record.getSbn(); 1548 if (!sbn.isAppGroup()) { 1549 return false; 1550 } 1551 1552 if (!sbn.getNotification().isGroupChild()) { 1553 return false; 1554 } 1555 1556 if (record.isCanceled) { 1557 return false; 1558 } 1559 1560 final NotificationSectioner sectioner = getSection(record); 1561 if (sectioner == null) { 1562 if (DEBUG) { 1563 Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); 1564 } 1565 return false; 1566 } 1567 1568 if (isNotificationAggregatedInSection(record, sectioner)) { 1569 return false; 1570 } 1571 1572 return true; 1573 } 1574 getNumChildrenForGroup(@onNull final String groupKey, final List<NotificationRecord> notificationList)1575 private static int getNumChildrenForGroup(@NonNull final String groupKey, 1576 final List<NotificationRecord> notificationList) { 1577 //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList 1578 int numChildren = 0; 1579 // find children for this summary 1580 for (NotificationRecord r : notificationList) { 1581 if (!r.getNotification().isGroupSummary() 1582 && groupKey.equals(r.getSbn().getGroup())) { 1583 numChildren++; 1584 } 1585 } 1586 1587 if (DEBUG) { 1588 Log.i(TAG, "getNumChildrenForGroup " + groupKey + " numChild: " + numChildren); 1589 } 1590 return numChildren; 1591 } 1592 isGroupSummaryWithoutChildren(final NotificationRecord record, final List<NotificationRecord> notificationList)1593 private static boolean isGroupSummaryWithoutChildren(final NotificationRecord record, 1594 final List<NotificationRecord> notificationList) { 1595 final StatusBarNotification sbn = record.getSbn(); 1596 final String groupKey = record.getSbn().getGroup(); 1597 1598 // ignore non app groups and non summaries 1599 if (!sbn.isAppGroup() || !record.getNotification().isGroupSummary()) { 1600 return false; 1601 } 1602 1603 return getNumChildrenForGroup(groupKey, notificationList) == 0; 1604 } 1605 isGroupChildWithoutSummary(final NotificationRecord record, final Map<String, NotificationRecord> summaryByGroupKey)1606 private static boolean isGroupChildWithoutSummary(final NotificationRecord record, 1607 final Map<String, NotificationRecord> summaryByGroupKey) { 1608 final StatusBarNotification sbn = record.getSbn(); 1609 final String groupKey = record.getSbn().getGroupKey(); 1610 1611 if (!sbn.isAppGroup()) { 1612 return false; 1613 } 1614 1615 if (record.getNotification().isGroupSummary()) { 1616 return false; 1617 } 1618 1619 if (summaryByGroupKey.containsKey(groupKey)) { 1620 return false; 1621 } 1622 1623 return true; 1624 } 1625 1626 @GuardedBy("mAggregatedNotifications") aggregateUngroupedNotifications(FullyQualifiedGroupKey fullAggregateGroupKey, String triggeringNotifKey, Map<String, NotificationAttributes> ungrouped, final boolean hasSummary, int summaryId)1627 private void aggregateUngroupedNotifications(FullyQualifiedGroupKey fullAggregateGroupKey, 1628 String triggeringNotifKey, Map<String, NotificationAttributes> ungrouped, 1629 final boolean hasSummary, int summaryId) { 1630 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 1631 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 1632 aggregatedNotificationsAttrs.putAll(ungrouped); 1633 mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); 1634 1635 // add/update aggregate summary 1636 updateAggregateAppGroup(fullAggregateGroupKey, triggeringNotifKey, hasSummary, summaryId); 1637 1638 // add notification to aggregate group 1639 for (String key: ungrouped.keySet()) { 1640 mCallback.addAutoGroup(key, fullAggregateGroupKey.toString(), true); 1641 } 1642 1643 //cleanup mUngroupedAbuseNotifications 1644 mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); 1645 } 1646 1647 @GuardedBy("mAggregatedNotifications") updateAggregateAppGroup(FullyQualifiedGroupKey fullAggregateGroupKey, String triggeringNotifKey, boolean hasSummary, int summaryId)1648 private void updateAggregateAppGroup(FullyQualifiedGroupKey fullAggregateGroupKey, 1649 String triggeringNotifKey, boolean hasSummary, int summaryId) { 1650 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 1651 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 1652 NotificationAttributes attr = getSummaryAttributes(fullAggregateGroupKey.pkg, 1653 aggregatedNotificationsAttrs); 1654 String channelId = hasSummary ? attr.channelId 1655 : aggregatedNotificationsAttrs.get(triggeringNotifKey).channelId; 1656 NotificationAttributes summaryAttr = new NotificationAttributes(attr.flags, attr.icon, 1657 attr.iconColor, attr.visibility, attr.groupAlertBehavior, channelId); 1658 1659 if (!hasSummary) { 1660 if (DEBUG) { 1661 Log.i(TAG, "Create aggregate summary: " + fullAggregateGroupKey); 1662 } 1663 mCallback.addAutoGroupSummary(fullAggregateGroupKey.userId, fullAggregateGroupKey.pkg, 1664 triggeringNotifKey, fullAggregateGroupKey.toString(), summaryId, summaryAttr); 1665 } else { 1666 if (DEBUG) { 1667 Log.i(TAG, "Update aggregate summary: " + fullAggregateGroupKey); 1668 } 1669 mCallback.updateAutogroupSummary(fullAggregateGroupKey.userId, 1670 fullAggregateGroupKey.pkg, fullAggregateGroupKey.toString(), summaryAttr); 1671 } 1672 } 1673 1674 @GuardedBy("mAggregatedNotifications") groupSparseGroups(final NotificationRecord record, final List<NotificationRecord> notificationList, final Map<String, NotificationRecord> summaryByGroupKey, final NotificationSectioner sectioner, final FullyQualifiedGroupKey fullAggregateGroupKey)1675 private void groupSparseGroups(final NotificationRecord record, 1676 final List<NotificationRecord> notificationList, 1677 final Map<String, NotificationRecord> summaryByGroupKey, 1678 final NotificationSectioner sectioner, 1679 final FullyQualifiedGroupKey fullAggregateGroupKey) { 1680 final ArrayMap<String, NotificationRecord> sparseGroupSummaries = getSparseGroups( 1681 fullAggregateGroupKey, notificationList, summaryByGroupKey, sectioner); 1682 if (sparseGroupSummaries.size() >= mAutogroupSparseGroupsAtCount) { 1683 if (DEBUG) { 1684 Log.i(TAG, 1685 "Aggregate sparse groups for: " + record.getSbn().getPackageName() 1686 + " Section: " + sectioner.mName); 1687 } 1688 1689 ArrayMap<String, NotificationAttributes> ungrouped = 1690 mUngroupedAbuseNotifications.getOrDefault( 1691 fullAggregateGroupKey, new ArrayMap<>()); 1692 final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = 1693 mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); 1694 final boolean hasSummary = !aggregatedNotificationsAttrs.isEmpty(); 1695 String triggeringKey = null; 1696 if (!record.getNotification().isGroupSummary()) { 1697 // Use this record as triggeringKey only if not a group summary (will be removed) 1698 triggeringKey = record.getKey(); 1699 } 1700 for (NotificationRecord r : notificationList) { 1701 // Add notifications for detected sparse groups 1702 if (sparseGroupSummaries.containsKey(r.getGroupKey())) { 1703 // Move child notifications to aggregate group 1704 if (!r.getNotification().isGroupSummary()) { 1705 if (DEBUG) { 1706 Log.i(TAG, "Aggregate notification (sparse group): " + r); 1707 } 1708 mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true); 1709 aggregatedNotificationsAttrs.put(r.getKey(), 1710 new NotificationAttributes(r.getFlags(), 1711 r.getNotification().getSmallIcon(), r.getNotification().color, 1712 r.getNotification().visibility, 1713 r.getNotification().getGroupAlertBehavior(), 1714 r.getChannel().getId())); 1715 1716 // Pick the first valid triggeringKey 1717 if (triggeringKey == null) { 1718 triggeringKey = r.getKey(); 1719 } 1720 } else if (r.getNotification().isGroupSummary()) { 1721 // Remove summary notifications 1722 if (DEBUG) { 1723 Log.i(TAG, "Remove app summary (sparse group): " + r); 1724 } 1725 mCallback.removeAppProvidedSummary(r.getKey()); 1726 cacheCanceledSummary(r); 1727 } 1728 } else { 1729 // Add any notifications left ungrouped 1730 if (ungrouped.containsKey(r.getKey())) { 1731 if (DEBUG) { 1732 Log.i(TAG, "Aggregate ungrouped (sparse group): " + r); 1733 } 1734 mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true); 1735 aggregatedNotificationsAttrs.put(r.getKey(),ungrouped.get(r.getKey())); 1736 } 1737 } 1738 } 1739 1740 mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); 1741 // add/update aggregate summary 1742 updateAggregateAppGroup(fullAggregateGroupKey, triggeringKey, hasSummary, 1743 sectioner.mSummaryId); 1744 1745 //cleanup mUngroupedAbuseNotifications 1746 mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); 1747 } 1748 } 1749 1750 @VisibleForTesting getSparseGroups( final FullyQualifiedGroupKey fullAggregateGroupKey, final List<NotificationRecord> notificationList, final Map<String, NotificationRecord> summaryByGroupKey, final NotificationSectioner sectioner)1751 protected ArrayMap<String, NotificationRecord> getSparseGroups( 1752 final FullyQualifiedGroupKey fullAggregateGroupKey, 1753 final List<NotificationRecord> notificationList, 1754 final Map<String, NotificationRecord> summaryByGroupKey, 1755 final NotificationSectioner sectioner) { 1756 ArrayMap<String, NotificationRecord> sparseGroups = new ArrayMap<>(); 1757 for (NotificationRecord summary : summaryByGroupKey.values()) { 1758 if (summary != null && sectioner.isInSection(summary)) { 1759 if (summary.getSbn().getPackageName().equalsIgnoreCase(fullAggregateGroupKey.pkg) 1760 && summary.getUserId() == fullAggregateGroupKey.userId 1761 && summary.getSbn().isAppGroup() 1762 && !summary.getGroupKey().equals(fullAggregateGroupKey.toString())) { 1763 int numChildren = getNumChildrenForGroupWithSection(summary.getSbn().getGroup(), 1764 notificationList, sectioner); 1765 if (numChildren > 0 && numChildren < MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING) { 1766 sparseGroups.put(summary.getGroupKey(), summary); 1767 } 1768 } 1769 } 1770 } 1771 return sparseGroups; 1772 } 1773 1774 /** 1775 * Get the number of children of a group if all match a certain section. 1776 * Used for force grouping sparse groups, where the summary may match a section but the 1777 * child notifications do not: ie. conversations 1778 * 1779 * @param groupKey the group key (name) 1780 * @param notificationList all notifications list 1781 * @param sectioner the section to match 1782 * @return number of children in that group or -1 if section does not match 1783 */ getNumChildrenForGroupWithSection(final String groupKey, final List<NotificationRecord> notificationList, final NotificationSectioner sectioner)1784 private int getNumChildrenForGroupWithSection(final String groupKey, 1785 final List<NotificationRecord> notificationList, 1786 final NotificationSectioner sectioner) { 1787 int numChildren = 0; 1788 for (NotificationRecord r : notificationList) { 1789 if (!r.getNotification().isGroupSummary() && groupKey.equals(r.getSbn().getGroup())) { 1790 NotificationSectioner childSection = getSection(r); 1791 if (childSection == null || childSection != sectioner) { 1792 if (DEBUG) { 1793 Slog.i(TAG, 1794 "getNumChildrenForGroupWithSection skip because invalid section: " 1795 + groupKey + " r: " + r); 1796 } 1797 return -1; 1798 } else { 1799 numChildren++; 1800 } 1801 } 1802 } 1803 1804 if (DEBUG) { 1805 Slog.i(TAG, 1806 "getNumChildrenForGroupWithSection " + groupKey + " numChild: " + numChildren); 1807 } 1808 return numChildren; 1809 } 1810 1811 @GuardedBy("mAggregatedNotifications") cacheCanceledSummary(NotificationRecord record)1812 private void cacheCanceledSummary(NotificationRecord record) { 1813 final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(), 1814 record.getSbn().getPackageName(), record.getNotification().getGroup()); 1815 mCanceledSummaries.put(groupKey, new CachedSummary( 1816 record.getSbn().getId(), 1817 record.getSbn().getTag(), 1818 record.getNotification().getGroup(), 1819 record.getKey(), 1820 record.getNotification().deleteIntent)); 1821 } 1822 1823 @GuardedBy("mAggregatedNotifications") maybeClearCanceledSummariesCache(String pkgName, int userId, String groupName, List<NotificationRecord> notificationList, boolean sendSummaryDelete)1824 private void maybeClearCanceledSummariesCache(String pkgName, int userId, 1825 String groupName, List<NotificationRecord> notificationList, 1826 boolean sendSummaryDelete) { 1827 final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, 1828 groupName); 1829 CachedSummary summary = mCanceledSummaries.get(findKey); 1830 // Check if any notifications from original group remain 1831 if (summary != null) { 1832 if (DEBUG) { 1833 Log.i(TAG, "Try removing cached summary: " + summary); 1834 } 1835 boolean stillHasChildren = false; 1836 //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList 1837 for (NotificationRecord r : notificationList) { 1838 if (summary.originalGroupKey.equals(r.getNotification().getGroup()) 1839 && r.getUser().getIdentifier() == userId 1840 && r.getSbn().getPackageName().equals(pkgName)) { 1841 stillHasChildren = true; 1842 break; 1843 } 1844 } 1845 if (!stillHasChildren) { 1846 removeCachedSummary(pkgName, userId, summary); 1847 if (sendSummaryDelete && summary.deleteIntent != null) { 1848 mCallback.sendAppProvidedSummaryDeleteIntent(pkgName, summary.deleteIntent); 1849 } 1850 } 1851 } 1852 } 1853 1854 @VisibleForTesting 1855 @GuardedBy("mAggregatedNotifications") findCanceledSummary(String pkgName, String tag, int id, int userId)1856 protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId) { 1857 for (FullyQualifiedGroupKey key: mCanceledSummaries.keySet()) { 1858 if (pkgName.equals(key.pkg) && userId == key.userId) { 1859 CachedSummary summary = mCanceledSummaries.get(key); 1860 if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) { 1861 return summary; 1862 } 1863 } 1864 } 1865 return null; 1866 } 1867 1868 @VisibleForTesting 1869 @GuardedBy("mAggregatedNotifications") findCanceledSummary(String pkgName, String tag, int id, int userId, String groupName)1870 protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId, 1871 String groupName) { 1872 final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, 1873 groupName); 1874 CachedSummary summary = mCanceledSummaries.get(findKey); 1875 if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) { 1876 return summary; 1877 } else { 1878 return null; 1879 } 1880 } 1881 1882 @GuardedBy("mAggregatedNotifications") removeCachedSummary(String pkgName, int userId, CachedSummary summary)1883 private void removeCachedSummary(String pkgName, int userId, CachedSummary summary) { 1884 final FullyQualifiedGroupKey key = new FullyQualifiedGroupKey(userId, pkgName, 1885 summary.originalGroupKey); 1886 mCanceledSummaries.remove(key); 1887 } 1888 isUpdateForCanceledSummary(final NotificationRecord record)1889 protected boolean isUpdateForCanceledSummary(final NotificationRecord record) { 1890 synchronized (mAggregatedNotifications) { 1891 if (record.getSbn().isAppGroup() && record.getNotification().isGroupSummary()) { 1892 CachedSummary cachedSummary = findCanceledSummary(record.getSbn().getPackageName(), 1893 record.getSbn().getTag(), record.getSbn().getId(), record.getUserId(), 1894 record.getNotification().getGroup()); 1895 return cachedSummary != null; 1896 } 1897 return false; 1898 } 1899 } 1900 1901 /** 1902 * Cancels the original group's children when an app cancels a summary that was 'maybe' 1903 * previously removed due to forced grouping of a "sparse group". 1904 * 1905 * @param pkgName packageName 1906 * @param tag original summary notification tag 1907 * @param id original summary notification id 1908 * @param userId original summary userId 1909 */ 1910 @FlaggedApi(Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS) maybeCancelGroupChildrenForCanceledSummary(String pkgName, String tag, int id, int userId, int cancelReason)1911 public void maybeCancelGroupChildrenForCanceledSummary(String pkgName, String tag, int id, 1912 int userId, int cancelReason) { 1913 synchronized (mAggregatedNotifications) { 1914 final CachedSummary summary = findCanceledSummary(pkgName, tag, id, userId); 1915 if (summary != null) { 1916 if (DEBUG) { 1917 Log.i(TAG, "Found cached summary: " + summary.key); 1918 } 1919 mCallback.removeNotificationFromCanceledGroup(userId, pkgName, 1920 summary.originalGroupKey, cancelReason); 1921 removeCachedSummary(pkgName, userId, summary); 1922 } 1923 } 1924 } 1925 getSection(final NotificationRecord record)1926 static NotificationSectioner getSection(final NotificationRecord record) { 1927 for (NotificationSectioner sectioner: NOTIFICATION_SHADE_SECTIONS) { 1928 if (sectioner.isInSection(record)) { 1929 return sectioner; 1930 } 1931 } 1932 return null; 1933 } 1934 FullyQualifiedGroupKey(int userId, String pkg, String groupName)1935 record FullyQualifiedGroupKey(int userId, String pkg, String groupName) { 1936 FullyQualifiedGroupKey(int userId, String pkg, @Nullable NotificationSectioner sectioner) { 1937 this(userId, pkg, AGGREGATE_GROUP_KEY + (sectioner != null ? sectioner.mName : "")); 1938 } 1939 1940 static FullyQualifiedGroupKey forRecord(NotificationRecord record, 1941 @Nullable NotificationSectioner sectioner) { 1942 return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(), 1943 sectioner); 1944 } 1945 1946 @Override 1947 public String toString() { 1948 return userId + "|" + pkg + "|" + "g:" + groupName; 1949 } 1950 } 1951 dump(PrintWriter pw, String prefix)1952 protected void dump(PrintWriter pw, String prefix) { 1953 synchronized (mAggregatedNotifications) { 1954 if (!mUngroupedAbuseNotifications.isEmpty()) { 1955 pw.println(prefix + "Ungrouped notifications:"); 1956 for (FullyQualifiedGroupKey groupKey: mUngroupedAbuseNotifications.keySet()) { 1957 if (!mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>()) 1958 .isEmpty()) { 1959 pw.println(prefix + prefix + groupKey.toString()); 1960 for (String notifKey : mUngroupedAbuseNotifications.get(groupKey) 1961 .keySet()) { 1962 pw.println(prefix + prefix + prefix + notifKey); 1963 } 1964 } 1965 } 1966 pw.println(""); 1967 } 1968 1969 if (!mAggregatedNotifications.isEmpty()) { 1970 pw.println(prefix + "Autogrouped notifications:"); 1971 for (FullyQualifiedGroupKey groupKey: mAggregatedNotifications.keySet()) { 1972 if (!mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()) 1973 .isEmpty()) { 1974 pw.println(prefix + prefix + groupKey.toString()); 1975 for (String notifKey : mAggregatedNotifications.get(groupKey).keySet()) { 1976 pw.println(prefix + prefix + prefix + notifKey); 1977 } 1978 } 1979 } 1980 pw.println(""); 1981 } 1982 1983 if (!mCanceledSummaries.isEmpty()) { 1984 pw.println(prefix + "Cached canceled summaries:"); 1985 for (CachedSummary summary: mCanceledSummaries.values()) { 1986 pw.println(prefix + prefix + prefix + summary.key + " -> " 1987 + summary.originalGroupKey); 1988 } 1989 pw.println(""); 1990 } 1991 } 1992 } 1993 1994 protected static class NotificationSectioner { 1995 final String mName; 1996 final int mSummaryId; 1997 private final Predicate<NotificationRecord> mSectionChecker; 1998 NotificationSectioner(String name, int summaryId, Predicate<NotificationRecord> sectionChecker)1999 private NotificationSectioner(String name, int summaryId, 2000 Predicate<NotificationRecord> sectionChecker) { 2001 mName = name; 2002 mSummaryId = summaryId; 2003 mSectionChecker = sectionChecker; 2004 } 2005 isInSection(final NotificationRecord record)2006 boolean isInSection(final NotificationRecord record) { 2007 return isNotificationGroupable(record) && mSectionChecker.test(record); 2008 } 2009 isNotificationGroupable(final NotificationRecord record)2010 private boolean isNotificationGroupable(final NotificationRecord record) { 2011 if (!Flags.notificationForceGroupConversations()) { 2012 if (record.isConversation()) { 2013 return false; 2014 } 2015 } 2016 2017 Notification notification = record.getSbn().getNotification(); 2018 boolean isColorizedFGS = notification.isForegroundService() 2019 && notification.isColorized() 2020 && record.getImportance() > NotificationManager.IMPORTANCE_MIN; 2021 boolean isCall = record.getImportance() > NotificationManager.IMPORTANCE_MIN 2022 && notification.isStyle(Notification.CallStyle.class); 2023 if (isColorizedFGS || isCall) { 2024 return false; 2025 } 2026 2027 if (record.getSbn().getNotification().isMediaNotification()) { 2028 return false; 2029 } 2030 2031 return true; 2032 } 2033 } 2034 CachedSummary(int id, String tag, String originalGroupKey, String key, @Nullable PendingIntent deleteIntent)2035 record CachedSummary(int id, String tag, String originalGroupKey, String key, 2036 @Nullable PendingIntent deleteIntent) { } 2037 2038 protected static class NotificationAttributes { 2039 public final int flags; 2040 public final int iconColor; 2041 public final Icon icon; 2042 public final int visibility; 2043 public final int groupAlertBehavior; 2044 public final String channelId; 2045 NotificationAttributes(int flags, Icon icon, int iconColor, int visibility, int groupAlertBehavior, String channelId)2046 public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility, 2047 int groupAlertBehavior, String channelId) { 2048 this.flags = flags; 2049 this.icon = icon; 2050 this.iconColor = iconColor; 2051 this.visibility = visibility; 2052 this.groupAlertBehavior = groupAlertBehavior; 2053 this.channelId = channelId; 2054 } 2055 NotificationAttributes(@onNull NotificationAttributes attr)2056 public NotificationAttributes(@NonNull NotificationAttributes attr) { 2057 this.flags = attr.flags; 2058 this.icon = attr.icon; 2059 this.iconColor = attr.iconColor; 2060 this.visibility = attr.visibility; 2061 this.groupAlertBehavior = attr.groupAlertBehavior; 2062 this.channelId = attr.channelId; 2063 } 2064 2065 @Override equals(Object o)2066 public boolean equals(Object o) { 2067 if (this == o) { 2068 return true; 2069 } 2070 if (!(o instanceof NotificationAttributes that)) { 2071 return false; 2072 } 2073 return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon) 2074 && visibility == that.visibility 2075 && groupAlertBehavior == that.groupAlertBehavior 2076 && channelId.equals(that.channelId); 2077 } 2078 2079 @Override hashCode()2080 public int hashCode() { 2081 return Objects.hash(flags, iconColor, icon, visibility, groupAlertBehavior, channelId); 2082 } 2083 2084 @Override toString()2085 public String toString() { 2086 return "NotificationAttributes: flags: " + flags + " icon: " + icon + " color: " 2087 + iconColor + " vis: " + visibility + " groupAlertBehavior: " 2088 + groupAlertBehavior + " channelId: " + channelId; 2089 } 2090 } 2091 2092 protected interface Callback { addAutoGroup(String key, String groupName, boolean requestSort)2093 void addAutoGroup(String key, String groupName, boolean requestSort); removeAutoGroup(String key)2094 void removeAutoGroup(String key); 2095 addAutoGroupSummary(int userId, String pkg, String triggeringKey, String groupName, int summaryId, NotificationAttributes summaryAttr)2096 void addAutoGroupSummary(int userId, String pkg, String triggeringKey, String groupName, 2097 int summaryId, NotificationAttributes summaryAttr); removeAutoGroupSummary(int user, String pkg, String groupKey)2098 void removeAutoGroupSummary(int user, String pkg, String groupKey); 2099 updateAutogroupSummary(int userId, String pkg, String groupKey, NotificationAttributes summaryAttr)2100 void updateAutogroupSummary(int userId, String pkg, String groupKey, 2101 NotificationAttributes summaryAttr); 2102 2103 // New callbacks for API abuse grouping removeAppProvidedSummary(String key)2104 void removeAppProvidedSummary(String key); 2105 2106 /** 2107 * Send a cached summary's deleteIntent, when the last of its original children is removed. 2108 * 2109 * <p>While technically the group summary was "canceled" much earlier (because it was the 2110 * summary of a sparse group and its children got reparented), the posting package expected 2111 * the summary's deleteIntent to fire when the summary is auto-dismissed. 2112 */ sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent)2113 void sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent); 2114 removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason)2115 void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, 2116 int cancelReason); 2117 2118 /** 2119 * Cancels the group summary of a notification that was regrouped because of classification 2120 * (bundling). Only cancels if the summary is the last notification of the original group. 2121 * @param triggeringKey the triggering child notification key 2122 * @param groupKey the original group key 2123 * @return the canceled group summary or null if the summary was not canceled 2124 */ 2125 @Nullable removeAppProvidedSummaryOnClassification(String triggeringKey, @Nullable String groupKey)2126 NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey, 2127 @Nullable String groupKey); 2128 } 2129 } 2130