Loading core/java/android/service/notification/flags.aconfig +8 −1 Original line number Diff line number Diff line Loading @@ -66,3 +66,10 @@ flag { description: "Allows the NAS to create and modify conversation notifications" bug: "373599715" } flag { name: "notification_regroup_on_classification" namespace: "systemui" description: "This flag controls regrouping after notification classification" bug: "372775153" } services/core/java/com/android/server/notification/GroupHelper.java +170 −4 Original line number Diff line number Diff line Loading @@ -25,8 +25,10 @@ import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_PUBLIC; import static android.service.notification.Flags.notificationForceGrouping; import static android.service.notification.Flags.notificationRegroupOnClassification; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; Loading @@ -49,6 +51,9 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; Loading Loading @@ -83,10 +88,22 @@ public class GroupHelper { // with less than this value, they will be forced grouped private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3; // Regrouping needed because the channel was updated, ie. importance changed static final int REGROUP_REASON_CHANNEL_UPDATE = 0; // Regrouping needed because of notification bundling static final int REGROUP_REASON_BUNDLE = 1; @IntDef(prefix = { "REGROUP_REASON_" }, value = { REGROUP_REASON_CHANNEL_UPDATE, REGROUP_REASON_BUNDLE, }) @Retention(RetentionPolicy.SOURCE) @interface RegroupingReason {} private final Callback mCallback; private final int mAutoGroupAtCount; private final int mAutogroupSparseGroupsAtCount; private final int mAutoGroupRegroupingAtCount; private final Context mContext; private final PackageManager mPackageManager; private boolean mIsTestHarnessExempted; Loading Loading @@ -173,6 +190,11 @@ public class GroupHelper { mContext = context; mPackageManager = packageManager; mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount; if (notificationRegroupOnClassification()) { mAutoGroupRegroupingAtCount = 1; } else { mAutoGroupRegroupingAtCount = mAutoGroupAtCount; } NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections(); } Loading Loading @@ -865,7 +887,8 @@ public class GroupHelper { } } regroupNotifications(userId, pkgName, notificationsToCheck); regroupNotifications(userId, pkgName, notificationsToCheck, REGROUP_REASON_CHANNEL_UPDATE); } } Loading @@ -883,13 +906,14 @@ public class GroupHelper { ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); notificationsToCheck.put(record.getKey(), record); regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), notificationsToCheck); notificationsToCheck, REGROUP_REASON_BUNDLE); } } @GuardedBy("mAggregatedNotifications") private void regroupNotifications(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { ArrayMap<String, NotificationRecord> notificationsToCheck, @RegroupingReason int regroupingReason) { // The list of notification operations required after the channel update final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); Loading @@ -904,12 +928,42 @@ public class GroupHelper { notificationsToMove.addAll( getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); // Handle "grouped correctly" notifications that were re-classified (bundled) if (notificationRegroupOnClassification()) { if (regroupingReason == REGROUP_REASON_BUNDLE) { notificationsToMove.addAll( getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); } } // Batch move to new section if (!notificationsToMove.isEmpty()) { moveNotificationsToNewSection(userId, pkgName, notificationsToMove); } } private List<NotificationMoveOp> getReclassifiedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); for (NotificationRecord record : notificationsToCheck.values()) { if (isChildOfValidAppGroup(record)) { // Check if section changes NotificationSectioner sectioner = getSection(record); if (sectioner != null) { FullyQualifiedGroupKey newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, sectioner); if (DEBUG) { Slog.v(TAG, "Regroup after classification: " + record + " to: " + newFullAggregateGroupKey); } notificationsToMove.add( new NotificationMoveOp(record, null, newFullAggregateGroupKey)); } } } return notificationsToMove; } @GuardedBy("mAggregatedNotifications") private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { Loading Loading @@ -1010,6 +1064,10 @@ public class GroupHelper { // Bundled operations to apply to groups affected by the channel update ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>(); // App-provided (valid) groups of notifications that were classified (bundled). // Summaries will be canceled if all child notifications have been bundled. ArrayMap<String, String> originalGroupsOfBundledNotifications = new ArrayMap<>(); for (NotificationMoveOp moveOp: notificationsToMove) { final NotificationRecord record = moveOp.record; final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup; Loading @@ -1035,6 +1093,13 @@ public class GroupHelper { groupsToUpdate.put(oldFullAggregateGroupKey, new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); } } else { if (notificationRegroupOnClassification()) { // Null "old aggregate group" => this notification was re-classified from // a valid app-provided group => maybe cancel the original summary // if no children are left originalGroupsOfBundledNotifications.put(record.getKey(), record.getGroupKey()); } } // Add moved notifications to the ungrouped list for new group and do grouping Loading Loading @@ -1076,7 +1141,7 @@ public class GroupHelper { NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; //Group needs to be created/updated if (ungrouped.size() >= mAutoGroupAtCount if (ungrouped.size() >= mAutoGroupRegroupingAtCount || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) { NotificationSectioner sectioner = getSection(triggeringNotification); if (sectioner == null) { Loading @@ -1092,6 +1157,18 @@ public class GroupHelper { } } } if (notificationRegroupOnClassification()) { // Cancel the summary if it's the last notification of the original app-provided group for (String triggeringKey : originalGroupsOfBundledNotifications.keySet()) { NotificationRecord canceledSummary = mCallback.removeAppProvidedSummaryOnClassification(triggeringKey, originalGroupsOfBundledNotifications.getOrDefault(triggeringKey, null)); if (canceledSummary != null) { cacheCanceledSummary(canceledSummary); } } } } static String getFullAggregateGroupKey(String pkgName, Loading @@ -1113,6 +1190,42 @@ public class GroupHelper { return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0; } private boolean isNotificationAggregatedInSection(NotificationRecord record, NotificationSectioner sectioner) { final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( record.getUserId(), record.getSbn().getPackageName(), sectioner); return record.getGroupKey().equals(fullAggregateGroupKey.toString()); } private boolean isChildOfValidAppGroup(NotificationRecord record) { final StatusBarNotification sbn = record.getSbn(); if (!sbn.isAppGroup()) { return false; } if (!sbn.getNotification().isGroupChild()) { return false; } if (record.isCanceled) { return false; } final NotificationSectioner sectioner = getSection(record); if (sectioner == null) { if (DEBUG) { Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); } return false; } if (isNotificationAggregatedInSection(record, sectioner)) { return false; } return true; } private static int getNumChildrenForGroup(@NonNull final String groupKey, final List<NotificationRecord> notificationList) { //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList Loading Loading @@ -1438,6 +1551,48 @@ public class GroupHelper { } } protected void dump(PrintWriter pw, String prefix) { synchronized (mAggregatedNotifications) { if (!mUngroupedAbuseNotifications.isEmpty()) { pw.println(prefix + "Ungrouped notifications:"); for (FullyQualifiedGroupKey groupKey: mUngroupedAbuseNotifications.keySet()) { if (!mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>()) .isEmpty()) { pw.println(prefix + prefix + groupKey.toString()); for (String notifKey : mUngroupedAbuseNotifications.get(groupKey) .keySet()) { pw.println(prefix + prefix + prefix + notifKey); } } } pw.println(""); } if (!mAggregatedNotifications.isEmpty()) { pw.println(prefix + "Autogrouped notifications:"); for (FullyQualifiedGroupKey groupKey: mAggregatedNotifications.keySet()) { if (!mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()) .isEmpty()) { pw.println(prefix + prefix + groupKey.toString()); for (String notifKey : mAggregatedNotifications.get(groupKey).keySet()) { pw.println(prefix + prefix + prefix + notifKey); } } } pw.println(""); } if (!mCanceledSummaries.isEmpty()) { pw.println(prefix + "Cached canceled summaries:"); for (CachedSummary summary: mCanceledSummaries.values()) { pw.println(prefix + prefix + prefix + summary.key + " -> " + summary.originalGroupKey); } pw.println(""); } } } protected static class NotificationSectioner { final String mName; final int mSummaryId; Loading Loading @@ -1551,5 +1706,16 @@ public class GroupHelper { void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason); /** * Cancels the group summary of a notification that was regrouped because of classification * (bundling). Only cancels if the summary is the last notification of the original group. * @param triggeringKey the triggering child notification key * @param groupKey the original group key * @return the canceled group summary or null if the summary was not canceled */ @Nullable NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey, @Nullable String groupKey); } } services/core/java/com/android/server/notification/NotificationManagerService.java +59 −0 Original line number Diff line number Diff line Loading @@ -3008,6 +3008,16 @@ public class NotificationManagerService extends SystemService { groupKey, REASON_APP_CANCEL, SystemClock.elapsedRealtime()); } } @Override @Nullable public NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey, @Nullable String oldGroupKey) { synchronized (mNotificationLock) { return removeAppProvidedSummaryOnClassificationLocked(triggeringKey, oldGroupKey); } } }); } Loading Loading @@ -7150,6 +7160,50 @@ public class NotificationManagerService extends SystemService { } } @GuardedBy("mNotificationLock") @Nullable NotificationRecord removeAppProvidedSummaryOnClassificationLocked(String triggeringKey, @Nullable String oldGroupKey) { NotificationRecord canceledSummary = null; NotificationRecord r = mNotificationsByKey.get(triggeringKey); if (r == null || oldGroupKey == null) { return null; } if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey); // We only care about app-provided valid groups if (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary)) { List<NotificationRecord> notificationsInGroup = findGroupNotificationsLocked(r.getSbn().getPackageName(), oldGroupKey, r.getUserId()); // Remove the app-provided summary if only the summary is left in the // original group, or summary + triggering notification that will be // regrouped boolean isOnlySummaryLeft = (notificationsInGroup.size() <= 1) || (notificationsInGroup.size() == 2 && notificationsInGroup.contains(r) && notificationsInGroup.contains(groupSummary)); if (isOnlySummaryLeft) { if (DBG) { Slog.i(TAG, "Removing app summary (all children bundled): " + groupSummary); } canceledSummary = groupSummary; mSummaryByGroupKey.remove(oldGroupKey); cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), groupSummary.getSbn().getPackageName(), groupSummary.getSbn().getTag(), groupSummary.getSbn().getId(), 0, 0, false, groupSummary.getUserId(), NotificationListenerService.REASON_GROUP_OPTIMIZATION, null); } } } return canceledSummary; } @GuardedBy("mNotificationLock") private boolean hasAutoGroupSummaryLocked(NotificationRecord record) { final String autbundledGroupKey; Loading Loading @@ -7530,6 +7584,11 @@ public class NotificationManagerService extends SystemService { mTtlHelper.dump(pw, " "); } } if (notificationForceGrouping()) { pw.println("\n GroupHelper:"); mGroupHelper.dump(pw, " "); } } } Loading services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +360 −5 File changed.Preview size limit exceeded, changes collapsed. Show changes services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +36 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; import static android.service.notification.Flags.FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; Loading Loading @@ -2682,6 +2683,41 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(0)); } @Test @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) public void testAggregateGroups_RemoveAppSummary_onClassification() throws Exception { final String originalGroupName = "originalGroup"; final int summaryId = 0; final NotificationRecord r1 = generateNotificationRecord(mTestNotificationChannel, summaryId + 1, originalGroupName, false); mService.addNotification(r1); final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel, summaryId + 2, originalGroupName, false); mService.addNotification(r2); final NotificationRecord summary = generateNotificationRecord(mTestNotificationChannel, summaryId, originalGroupName, true); mService.addNotification(summary); final String originalGroupKey = summary.getGroupKey(); assertThat(mService.mSummaryByGroupKey).containsEntry(originalGroupKey, summary); // Regroup first child notification r1.setOverrideGroupKey("newGroup"); // Check that removeAppProvidedSummaryOnClassificationLocked is null // => there is still one child left in the original group assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r1.getKey(), originalGroupKey)).isNull(); // Regroup last child notification r2.setOverrideGroupKey("newGroup"); // Check that removeAppProvidedSummaryOnClassificationLocked returns the original summary // and that the original app-provided summary is canceled assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r2.getKey(), originalGroupKey)).isEqualTo(summary); waitForIdle(); verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(summaryId)); assertThat(mService.mSummaryByGroupKey).doesNotContainKey(originalGroupKey); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testUngroupingAggregateSummary() throws Exception { Loading Loading
core/java/android/service/notification/flags.aconfig +8 −1 Original line number Diff line number Diff line Loading @@ -66,3 +66,10 @@ flag { description: "Allows the NAS to create and modify conversation notifications" bug: "373599715" } flag { name: "notification_regroup_on_classification" namespace: "systemui" description: "This flag controls regrouping after notification classification" bug: "372775153" }
services/core/java/com/android/server/notification/GroupHelper.java +170 −4 Original line number Diff line number Diff line Loading @@ -25,8 +25,10 @@ import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_PUBLIC; import static android.service.notification.Flags.notificationForceGrouping; import static android.service.notification.Flags.notificationRegroupOnClassification; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; Loading @@ -49,6 +51,9 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; Loading Loading @@ -83,10 +88,22 @@ public class GroupHelper { // with less than this value, they will be forced grouped private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3; // Regrouping needed because the channel was updated, ie. importance changed static final int REGROUP_REASON_CHANNEL_UPDATE = 0; // Regrouping needed because of notification bundling static final int REGROUP_REASON_BUNDLE = 1; @IntDef(prefix = { "REGROUP_REASON_" }, value = { REGROUP_REASON_CHANNEL_UPDATE, REGROUP_REASON_BUNDLE, }) @Retention(RetentionPolicy.SOURCE) @interface RegroupingReason {} private final Callback mCallback; private final int mAutoGroupAtCount; private final int mAutogroupSparseGroupsAtCount; private final int mAutoGroupRegroupingAtCount; private final Context mContext; private final PackageManager mPackageManager; private boolean mIsTestHarnessExempted; Loading Loading @@ -173,6 +190,11 @@ public class GroupHelper { mContext = context; mPackageManager = packageManager; mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount; if (notificationRegroupOnClassification()) { mAutoGroupRegroupingAtCount = 1; } else { mAutoGroupRegroupingAtCount = mAutoGroupAtCount; } NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections(); } Loading Loading @@ -865,7 +887,8 @@ public class GroupHelper { } } regroupNotifications(userId, pkgName, notificationsToCheck); regroupNotifications(userId, pkgName, notificationsToCheck, REGROUP_REASON_CHANNEL_UPDATE); } } Loading @@ -883,13 +906,14 @@ public class GroupHelper { ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); notificationsToCheck.put(record.getKey(), record); regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), notificationsToCheck); notificationsToCheck, REGROUP_REASON_BUNDLE); } } @GuardedBy("mAggregatedNotifications") private void regroupNotifications(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { ArrayMap<String, NotificationRecord> notificationsToCheck, @RegroupingReason int regroupingReason) { // The list of notification operations required after the channel update final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); Loading @@ -904,12 +928,42 @@ public class GroupHelper { notificationsToMove.addAll( getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); // Handle "grouped correctly" notifications that were re-classified (bundled) if (notificationRegroupOnClassification()) { if (regroupingReason == REGROUP_REASON_BUNDLE) { notificationsToMove.addAll( getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); } } // Batch move to new section if (!notificationsToMove.isEmpty()) { moveNotificationsToNewSection(userId, pkgName, notificationsToMove); } } private List<NotificationMoveOp> getReclassifiedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); for (NotificationRecord record : notificationsToCheck.values()) { if (isChildOfValidAppGroup(record)) { // Check if section changes NotificationSectioner sectioner = getSection(record); if (sectioner != null) { FullyQualifiedGroupKey newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, sectioner); if (DEBUG) { Slog.v(TAG, "Regroup after classification: " + record + " to: " + newFullAggregateGroupKey); } notificationsToMove.add( new NotificationMoveOp(record, null, newFullAggregateGroupKey)); } } } return notificationsToMove; } @GuardedBy("mAggregatedNotifications") private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { Loading Loading @@ -1010,6 +1064,10 @@ public class GroupHelper { // Bundled operations to apply to groups affected by the channel update ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>(); // App-provided (valid) groups of notifications that were classified (bundled). // Summaries will be canceled if all child notifications have been bundled. ArrayMap<String, String> originalGroupsOfBundledNotifications = new ArrayMap<>(); for (NotificationMoveOp moveOp: notificationsToMove) { final NotificationRecord record = moveOp.record; final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup; Loading @@ -1035,6 +1093,13 @@ public class GroupHelper { groupsToUpdate.put(oldFullAggregateGroupKey, new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); } } else { if (notificationRegroupOnClassification()) { // Null "old aggregate group" => this notification was re-classified from // a valid app-provided group => maybe cancel the original summary // if no children are left originalGroupsOfBundledNotifications.put(record.getKey(), record.getGroupKey()); } } // Add moved notifications to the ungrouped list for new group and do grouping Loading Loading @@ -1076,7 +1141,7 @@ public class GroupHelper { NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; //Group needs to be created/updated if (ungrouped.size() >= mAutoGroupAtCount if (ungrouped.size() >= mAutoGroupRegroupingAtCount || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) { NotificationSectioner sectioner = getSection(triggeringNotification); if (sectioner == null) { Loading @@ -1092,6 +1157,18 @@ public class GroupHelper { } } } if (notificationRegroupOnClassification()) { // Cancel the summary if it's the last notification of the original app-provided group for (String triggeringKey : originalGroupsOfBundledNotifications.keySet()) { NotificationRecord canceledSummary = mCallback.removeAppProvidedSummaryOnClassification(triggeringKey, originalGroupsOfBundledNotifications.getOrDefault(triggeringKey, null)); if (canceledSummary != null) { cacheCanceledSummary(canceledSummary); } } } } static String getFullAggregateGroupKey(String pkgName, Loading @@ -1113,6 +1190,42 @@ public class GroupHelper { return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0; } private boolean isNotificationAggregatedInSection(NotificationRecord record, NotificationSectioner sectioner) { final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( record.getUserId(), record.getSbn().getPackageName(), sectioner); return record.getGroupKey().equals(fullAggregateGroupKey.toString()); } private boolean isChildOfValidAppGroup(NotificationRecord record) { final StatusBarNotification sbn = record.getSbn(); if (!sbn.isAppGroup()) { return false; } if (!sbn.getNotification().isGroupChild()) { return false; } if (record.isCanceled) { return false; } final NotificationSectioner sectioner = getSection(record); if (sectioner == null) { if (DEBUG) { Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); } return false; } if (isNotificationAggregatedInSection(record, sectioner)) { return false; } return true; } private static int getNumChildrenForGroup(@NonNull final String groupKey, final List<NotificationRecord> notificationList) { //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList Loading Loading @@ -1438,6 +1551,48 @@ public class GroupHelper { } } protected void dump(PrintWriter pw, String prefix) { synchronized (mAggregatedNotifications) { if (!mUngroupedAbuseNotifications.isEmpty()) { pw.println(prefix + "Ungrouped notifications:"); for (FullyQualifiedGroupKey groupKey: mUngroupedAbuseNotifications.keySet()) { if (!mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>()) .isEmpty()) { pw.println(prefix + prefix + groupKey.toString()); for (String notifKey : mUngroupedAbuseNotifications.get(groupKey) .keySet()) { pw.println(prefix + prefix + prefix + notifKey); } } } pw.println(""); } if (!mAggregatedNotifications.isEmpty()) { pw.println(prefix + "Autogrouped notifications:"); for (FullyQualifiedGroupKey groupKey: mAggregatedNotifications.keySet()) { if (!mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()) .isEmpty()) { pw.println(prefix + prefix + groupKey.toString()); for (String notifKey : mAggregatedNotifications.get(groupKey).keySet()) { pw.println(prefix + prefix + prefix + notifKey); } } } pw.println(""); } if (!mCanceledSummaries.isEmpty()) { pw.println(prefix + "Cached canceled summaries:"); for (CachedSummary summary: mCanceledSummaries.values()) { pw.println(prefix + prefix + prefix + summary.key + " -> " + summary.originalGroupKey); } pw.println(""); } } } protected static class NotificationSectioner { final String mName; final int mSummaryId; Loading Loading @@ -1551,5 +1706,16 @@ public class GroupHelper { void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason); /** * Cancels the group summary of a notification that was regrouped because of classification * (bundling). Only cancels if the summary is the last notification of the original group. * @param triggeringKey the triggering child notification key * @param groupKey the original group key * @return the canceled group summary or null if the summary was not canceled */ @Nullable NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey, @Nullable String groupKey); } }
services/core/java/com/android/server/notification/NotificationManagerService.java +59 −0 Original line number Diff line number Diff line Loading @@ -3008,6 +3008,16 @@ public class NotificationManagerService extends SystemService { groupKey, REASON_APP_CANCEL, SystemClock.elapsedRealtime()); } } @Override @Nullable public NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey, @Nullable String oldGroupKey) { synchronized (mNotificationLock) { return removeAppProvidedSummaryOnClassificationLocked(triggeringKey, oldGroupKey); } } }); } Loading Loading @@ -7150,6 +7160,50 @@ public class NotificationManagerService extends SystemService { } } @GuardedBy("mNotificationLock") @Nullable NotificationRecord removeAppProvidedSummaryOnClassificationLocked(String triggeringKey, @Nullable String oldGroupKey) { NotificationRecord canceledSummary = null; NotificationRecord r = mNotificationsByKey.get(triggeringKey); if (r == null || oldGroupKey == null) { return null; } if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey); // We only care about app-provided valid groups if (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary)) { List<NotificationRecord> notificationsInGroup = findGroupNotificationsLocked(r.getSbn().getPackageName(), oldGroupKey, r.getUserId()); // Remove the app-provided summary if only the summary is left in the // original group, or summary + triggering notification that will be // regrouped boolean isOnlySummaryLeft = (notificationsInGroup.size() <= 1) || (notificationsInGroup.size() == 2 && notificationsInGroup.contains(r) && notificationsInGroup.contains(groupSummary)); if (isOnlySummaryLeft) { if (DBG) { Slog.i(TAG, "Removing app summary (all children bundled): " + groupSummary); } canceledSummary = groupSummary; mSummaryByGroupKey.remove(oldGroupKey); cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), groupSummary.getSbn().getPackageName(), groupSummary.getSbn().getTag(), groupSummary.getSbn().getId(), 0, 0, false, groupSummary.getUserId(), NotificationListenerService.REASON_GROUP_OPTIMIZATION, null); } } } return canceledSummary; } @GuardedBy("mNotificationLock") private boolean hasAutoGroupSummaryLocked(NotificationRecord record) { final String autbundledGroupKey; Loading Loading @@ -7530,6 +7584,11 @@ public class NotificationManagerService extends SystemService { mTtlHelper.dump(pw, " "); } } if (notificationForceGrouping()) { pw.println("\n GroupHelper:"); mGroupHelper.dump(pw, " "); } } } Loading
services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +360 −5 File changed.Preview size limit exceeded, changes collapsed. Show changes
services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +36 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; import static android.service.notification.Flags.FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; Loading Loading @@ -2682,6 +2683,41 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(0)); } @Test @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) public void testAggregateGroups_RemoveAppSummary_onClassification() throws Exception { final String originalGroupName = "originalGroup"; final int summaryId = 0; final NotificationRecord r1 = generateNotificationRecord(mTestNotificationChannel, summaryId + 1, originalGroupName, false); mService.addNotification(r1); final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel, summaryId + 2, originalGroupName, false); mService.addNotification(r2); final NotificationRecord summary = generateNotificationRecord(mTestNotificationChannel, summaryId, originalGroupName, true); mService.addNotification(summary); final String originalGroupKey = summary.getGroupKey(); assertThat(mService.mSummaryByGroupKey).containsEntry(originalGroupKey, summary); // Regroup first child notification r1.setOverrideGroupKey("newGroup"); // Check that removeAppProvidedSummaryOnClassificationLocked is null // => there is still one child left in the original group assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r1.getKey(), originalGroupKey)).isNull(); // Regroup last child notification r2.setOverrideGroupKey("newGroup"); // Check that removeAppProvidedSummaryOnClassificationLocked returns the original summary // and that the original app-provided summary is canceled assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r2.getKey(), originalGroupKey)).isEqualTo(summary); waitForIdle(); verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(summaryId)); assertThat(mService.mSummaryByGroupKey).doesNotContainKey(originalGroupKey); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testUngroupingAggregateSummary() throws Exception { Loading