Notification API hardening: forced auto-grouping

 NotificationManagerService will post a group summary on the app's behalf
 and group notifications based on several scenarios:

  1. Groups without summary
  2. Summaries without child notifications
  3. Singleton groups (summaries with single or low number of children)

Flag: android.service.notification.notification_force_grouping
Flag: com.android.server.notification.notification_force_group_singletons

Test: atest GroupHelperTest NotificationManagerServiceTest

Bug: 336488844
Change-Id: Ie1c0727c5de5e3dc55ca2a1c177d9b4660ad588d
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index aea15e1..007fa5d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -4123,6 +4123,17 @@
     }
 
     /**
+     * Sets which type of notifications in a group are responsible for audibly alerting the
+     * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN},
+     * {@link #GROUP_ALERT_SUMMARY}.
+     * @param groupAlertBehavior
+     * @hide
+     */
+    public void setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) {
+        mGroupAlertBehavior = groupAlertBehavior;
+    }
+
+    /**
      * Returns the bubble metadata that will be used to display app content in a floating window
      * over the existing foreground activity.
      */
diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java
index 264b53c..146c2b6 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -176,7 +176,11 @@
 
     private String groupKey() {
         if (overrideGroupKey != null) {
-            return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey;
+            if (Flags.notificationForceGrouping()) {
+                return overrideGroupKey;
+            } else {
+                return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey;
+            }
         }
         final String group = getNotification().getGroup();
         final String sortKey = getNotification().getSortKey();
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index bdef041..51961a8 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -43,4 +43,18 @@
     namespace: "systemui"
     description: "Allows the NAS to classify notifications"
     bug: "343988084"
+}
+
+flag {
+  name: "notification_force_grouping"
+  namespace: "systemui"
+  description: "This flag controls the forced auto-grouping feature"
+  bug: "336488844"
+}
+
+flag {
+  name: "notification_silent_flag"
+  namespace: "systemui"
+  description: "Guards the new FLAG_SILENT Notification flag"
+  bug: "336488844"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 13cc99c..1cdab44 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -24,9 +24,14 @@
 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 android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -34,7 +39,9 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Slog;
 
 import com.android.internal.R;
@@ -42,14 +49,20 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * NotificationManagerService helper for auto-grouping notifications.
  */
 public class GroupHelper {
     private static final String TAG = "GroupHelper";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     protected static final String AUTOGROUP_KEY = "ranker_group";
 
@@ -63,8 +76,16 @@
     // Flags that autogroup summaries inherits if any child has them
     private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR;
 
+    protected static final String AGGREGATE_GROUP_KEY = "Aggregate_";
+
+    // If an app posts more than NotificationManagerService.AUTOGROUP_SPARSE_GROUPS_AT_COUNT groups
+    //  with less than this value, they will be forced grouped
+    private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3;
+
+
     private final Callback mCallback;
     private final int mAutoGroupAtCount;
+    private final int mAutogroupSparseGroupsAtCount;
     private final Context mContext;
     private final PackageManager mPackageManager;
 
@@ -75,12 +96,41 @@
     private final ArrayMap<String, ArrayMap<String, NotificationAttributes>> mUngroupedNotifications
             = new ArrayMap<>();
 
+    // Contains the list of notifications that should be aggregated (forced grouping)
+    // but there are less than mAutoGroupAtCount per section for a package.
+    // The primary map's key is the full aggregated group key: userId|pkgName|g:groupName
+    // The internal map's key is the notification record key
+    @GuardedBy("mAggregatedNotifications")
+    private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
+            mUngroupedAbuseNotifications = new ArrayMap<>();
+
+    // Contains the list of group summaries that were canceled when "singleton groups" were
+    // force grouped. Used to remove the original group's children when an app cancels the
+    // already removed summary. Key is userId|packageName|g:OriginalGroupName
+    @GuardedBy("mAggregatedNotifications")
+    private final ArrayMap<FullyQualifiedGroupKey, CachedSummary>
+            mCanceledSummaries = new ArrayMap<>();
+
+    // Represents the current state of the aggregated (forced grouped) notifications
+    // Key is the full aggregated group key: userId|pkgName|g:groupName
+    // And groupName is "Aggregate_"+sectionName
+    @GuardedBy("mAggregatedNotifications")
+    private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
+            mAggregatedNotifications = new ArrayMap<>();
+
+    private static final List<NotificationSectioner> NOTIFICATION_SHADE_SECTIONS = List.of(
+        new NotificationSectioner("AlertingSection", 0, (record) ->
+            record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT),
+        new NotificationSectioner("SilentSection", 1, (record) ->
+            record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT));
+
     public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount,
-            Callback callback) {
+            int autoGroupSparseGroupsAtCount, Callback callback) {
         mAutoGroupAtCount = autoGroupAtCount;
         mCallback =  callback;
         mContext = context;
         mPackageManager = packageManager;
+        mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount;
     }
 
     private String generatePackageKey(int userId, String pkg) {
@@ -88,40 +138,50 @@
     }
 
     @VisibleForTesting
-    @GuardedBy("mUngroupedNotifications")
-    protected int getAutogroupSummaryFlags(
-            @NonNull final ArrayMap<String, NotificationAttributes> children) {
+    protected static int getAutogroupSummaryFlags(
+            @NonNull final ArrayMap<String, NotificationAttributes> childrenMap) {
+        final Collection<NotificationAttributes> children = childrenMap.values();
         boolean allChildrenHasFlag = children.size() > 0;
         int anyChildFlagSet = 0;
-        for (int i = 0; i < children.size(); i++) {
-            if (!hasAnyFlag(children.valueAt(i).flags, ALL_CHILDREN_FLAG)) {
+        for (NotificationAttributes childAttr: children) {
+            if (!hasAnyFlag(childAttr.flags, ALL_CHILDREN_FLAG)) {
                 allChildrenHasFlag = false;
             }
-            if (hasAnyFlag(children.valueAt(i).flags, ANY_CHILDREN_FLAGS)) {
-                anyChildFlagSet |= (children.valueAt(i).flags & ANY_CHILDREN_FLAGS);
+            if (hasAnyFlag(childAttr.flags, ANY_CHILDREN_FLAGS)) {
+                anyChildFlagSet |= (childAttr.flags & ANY_CHILDREN_FLAGS);
             }
         }
         return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet;
     }
 
-    private boolean hasAnyFlag(int flags, int mask) {
+    private static boolean hasAnyFlag(int flags, int mask) {
         return (flags & mask) != 0;
     }
 
     /**
      * Called when a notification is newly posted. Checks whether that notification, and all other
      * active notifications should be grouped or ungrouped atuomatically, and returns whether.
-     * @param sbn The posted notification.
+     * @param record The posted notification.
      * @param autogroupSummaryExists Whether a summary for this notification already exists.
      * @return Whether the provided notification should be autogrouped synchronously.
      */
-    public boolean onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+    public boolean onNotificationPosted(NotificationRecord record, boolean autogroupSummaryExists) {
         boolean sbnToBeAutogrouped = false;
         try {
-            if (!sbn.isAppGroup()) {
-                sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists);
+            if (notificationForceGrouping()) {
+                final StatusBarNotification sbn = record.getSbn();
+                if (!sbn.isAppGroup()) {
+                    sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists);
+                } else {
+                    maybeUngroupWithSections(record);
+                }
             } else {
-                maybeUngroup(sbn, false, sbn.getUserId());
+                final StatusBarNotification sbn = record.getSbn();
+                if (!sbn.isAppGroup()) {
+                    sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists);
+                } else {
+                    maybeUngroup(sbn, false, sbn.getUserId());
+                }
             }
         } catch (Exception e) {
             Slog.e(TAG, "Failure processing new notification", e);
@@ -129,9 +189,20 @@
         return sbnToBeAutogrouped;
     }
 
-    public void onNotificationRemoved(StatusBarNotification sbn) {
+    /**
+     * Called when a notification was removed. Checks if that notification was part of an autogroup
+     * and triggers any necessary cleanups: summary removal, clearing caches etc.
+     *
+     * @param record The removed notification.
+     */
+    public void onNotificationRemoved(NotificationRecord record) {
         try {
-            maybeUngroup(sbn, true, sbn.getUserId());
+            if (notificationForceGrouping()) {
+                onNotificationRemoved(record, new ArrayList<>());
+            } else {
+                final StatusBarNotification sbn = record.getSbn();
+                maybeUngroup(sbn, true, sbn.getUserId());
+            }
         } catch (Exception e) {
             Slog.e(TAG, "Error processing canceled notification", e);
         }
@@ -156,10 +227,10 @@
             String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
             final ArrayMap<String, NotificationAttributes> children =
                     mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>());
-
             NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags,
                     sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
-                    sbn.getNotification().visibility);
+                    sbn.getNotification().visibility, Notification.GROUP_ALERT_CHILDREN,
+                    sbn.getNotification().getChannelId());
             children.put(sbn.getKey(), attr);
             mUngroupedNotifications.put(packageKey, children);
 
@@ -173,17 +244,20 @@
             if (autogroupSummaryExists) {
                 NotificationAttributes attr = new NotificationAttributes(flags,
                         sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
-                        VISIBILITY_PRIVATE);
+                        VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN,
+                        sbn.getNotification().getChannelId());
                 if (Flags.autogroupSummaryIconUpdate()) {
                     attr = updateAutobundledSummaryAttributes(sbn.getPackageName(), childrenAttr,
                             attr);
                 }
 
-                mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), attr);
+                mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(),
+                        AUTOGROUP_KEY, attr);
             } else {
                 Icon summaryIcon = sbn.getNotification().getSmallIcon();
                 int summaryIconColor = sbn.getNotification().color;
                 int summaryVisibility = VISIBILITY_PRIVATE;
+                String summaryChannelId = sbn.getNotification().getChannelId();
                 if (Flags.autogroupSummaryIconUpdate()) {
                     // Calculate the initial summary icon, icon color and visibility
                     NotificationAttributes iconAttr = getAutobundledSummaryAttributes(
@@ -191,12 +265,14 @@
                     summaryIcon = iconAttr.icon;
                     summaryIconColor = iconAttr.iconColor;
                     summaryVisibility = iconAttr.visibility;
+                    summaryChannelId = iconAttr.channelId;
                 }
 
                 NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon,
-                        summaryIconColor, summaryVisibility);
+                        summaryIconColor, summaryVisibility, Notification.GROUP_ALERT_CHILDREN,
+                        summaryChannelId);
                 mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(),
-                        attr);
+                        AUTOGROUP_KEY, Integer.MAX_VALUE, attr);
             }
             for (String keyToGroup : notificationsToGroup) {
                 if (android.app.Flags.checkAutogroupBeforePost()) {
@@ -204,10 +280,10 @@
                         // Autogrouping for the provided notification is to be done synchronously.
                         sbnToBeAutogrouped = true;
                     } else {
-                        mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+                        mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true);
                     }
                 } else {
-                    mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+                    mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true);
                 }
             }
         }
@@ -263,11 +339,12 @@
         }
 
         if (removeSummary) {
-            mCallback.removeAutoGroupSummary(userId, sbn.getPackageName());
+            mCallback.removeAutoGroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY);
         } else {
             NotificationAttributes attr = new NotificationAttributes(summaryFlags,
                     sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
-                    VISIBILITY_PRIVATE);
+                    VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN,
+                    sbn.getNotification().getChannelId());
             boolean attributesUpdated = false;
             if (Flags.autogroupSummaryIconUpdate()) {
                 NotificationAttributes newAttr = updateAutobundledSummaryAttributes(
@@ -279,7 +356,7 @@
             }
 
             if (updateSummaryFlags || attributesUpdated) {
-                mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), attr);
+                mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY, attr);
             }
         }
         if (removeAutogroupOverlay) {
@@ -287,16 +364,6 @@
         }
     }
 
-    @VisibleForTesting
-    int getNotGroupedByAppCount(int userId, String pkg) {
-        synchronized (mUngroupedNotifications) {
-            String key = generatePackageKey(userId, pkg);
-            final ArrayMap<String, NotificationAttributes> children =
-                    mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
-            return children.size();
-        }
-    }
-
     NotificationAttributes getAutobundledSummaryAttributes(@NonNull String packageName,
             @NonNull List<NotificationAttributes> childrenAttr) {
         Icon newIcon = null;
@@ -338,7 +405,20 @@
             newColor = COLOR_DEFAULT;
         }
 
-        return new NotificationAttributes(0, newIcon, newColor, newVisibility);
+        // Use GROUP_ALERT_CHILDREN
+        // Unless all children have GROUP_ALERT_SUMMARY => avoid muting all notifications in group
+        int newGroupAlertBehavior = Notification.GROUP_ALERT_SUMMARY;
+        for (NotificationAttributes attr: childrenAttr) {
+            if (attr.groupAlertBehavior != Notification.GROUP_ALERT_SUMMARY) {
+                newGroupAlertBehavior = Notification.GROUP_ALERT_CHILDREN;
+                break;
+            }
+        }
+
+        String channelId = !childrenAttr.isEmpty() ? childrenAttr.get(0).channelId : null;
+
+        return new NotificationAttributes(0, newIcon, newColor, newVisibility,
+                newGroupAlertBehavior, channelId);
     }
 
     NotificationAttributes updateAutobundledSummaryAttributes(@NonNull String packageName,
@@ -348,14 +428,28 @@
                 childrenAttr);
         Icon newIcon = newAttr.icon;
         int newColor = newAttr.iconColor;
+        String newChannelId = newAttr.channelId;
         if (newAttr.icon == null) {
             newIcon = oldAttr.icon;
         }
         if (newAttr.iconColor == Notification.COLOR_INVALID) {
             newColor = oldAttr.iconColor;
         }
+        if (newAttr.channelId == null) {
+            newChannelId = oldAttr.channelId;
+        }
 
-        return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility);
+        return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility,
+                oldAttr.groupAlertBehavior, newChannelId);
+    }
+
+    private NotificationAttributes getSummaryAttributes(String pkgName,
+            ArrayMap<String, NotificationAttributes> childrenMap) {
+        int flags = getAutogroupSummaryFlags(childrenMap);
+        NotificationAttributes attr = getAutobundledSummaryAttributes(pkgName,
+                childrenMap.values().stream().toList());
+        return new NotificationAttributes(flags, attr.icon, attr.iconColor, attr.visibility,
+                attr.groupAlertBehavior, attr.channelId);
     }
 
     /**
@@ -388,17 +482,865 @@
         }
     }
 
+    /**
+     * A non-app grouped notification has been added or updated
+     * Evaluate if:
+     * (a) an existing autogroup summary needs updated attributes
+     * (b) a new autogroup summary needs to be added with correct attributes
+     * (c) other non-app grouped children need to be moved to the autogroup
+     *
+     * This method implements autogrouping with sections support.
+     *
+     * And stores the list of upgrouped notifications & their flags
+     */
+    private boolean maybeGroupWithSections(NotificationRecord record,
+            boolean autogroupSummaryExists) {
+        final StatusBarNotification sbn = record.getSbn();
+        boolean sbnToBeAutogrouped = false;
+
+        final NotificationSectioner sectioner = getSection(record);
+        if (sectioner == null) {
+            if (DEBUG) {
+                Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
+            }
+            return false;
+        }
+
+        final String pkgName = sbn.getPackageName();
+        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
+                record.getUserId(), pkgName, sectioner);
+
+        // This notification is already aggregated
+        if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
+            return false;
+        }
+
+        synchronized (mAggregatedNotifications) {
+            ArrayMap<String, NotificationAttributes> ungrouped =
+                mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            ungrouped.put(record.getKey(), new NotificationAttributes(
+                record.getFlags(),
+                record.getNotification().getSmallIcon(),
+                record.getNotification().color,
+                record.getNotification().visibility,
+                record.getNotification().getGroupAlertBehavior(),
+                record.getChannel().getId()));
+            mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped);
+
+            // scenario 0: ungrouped notifications
+            if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
+                if (DEBUG) {
+                    if (ungrouped.size() >= mAutoGroupAtCount) {
+                        Log.i(TAG,
+                            "Found >=" + mAutoGroupAtCount
+                                + " ungrouped notifications => force grouping");
+                    } else {
+                        Log.i(TAG, "Found aggregate summary => force grouping");
+                    }
+                }
+
+                final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                    mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+                aggregatedNotificationsAttrs.putAll(ungrouped);
+                mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
+
+                // add/update aggregate summary
+                updateAggregateAppGroup(fullAggregateGroupKey, record.getKey(),
+                        autogroupSummaryExists, sectioner.mSummaryId);
+
+                // add notification to aggregate group
+                for (String keyToGroup : ungrouped.keySet()) {
+                    if (android.app.Flags.checkAutogroupBeforePost()) {
+                        if (keyToGroup.equals(record.getKey())) {
+                            // Autogrouping for the posted notification is to be done synchronously.
+                            sbnToBeAutogrouped = true;
+                        } else {
+                            mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(),
+                                    true);
+                        }
+                    } else {
+                        mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(), true);
+                    }
+                }
+
+                //cleanup mUngroupedAbuseNotifications
+                mUngroupedAbuseNotifications.remove(fullAggregateGroupKey);
+            }
+        }
+
+        return sbnToBeAutogrouped;
+    }
+
+    /**
+     * A notification was added that's app grouped.
+     * Evaluate whether:
+     * (a) an existing autogroup summary needs updated attributes
+     * (b) if we need to remove our autogroup overlay for this notification
+     * (c) we need to remove the autogroup summary
+     *
+     * This method implements autogrouping with sections support.
+     *
+     * And updates the internal state of un-app-grouped notifications and their flags.
+     */
+    private void maybeUngroupWithSections(NotificationRecord record) {
+        final StatusBarNotification sbn = record.getSbn();
+        final String pkgName = sbn.getPackageName();
+        final int userId = record.getUserId();
+        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,
+                pkgName, getSection(record));
+
+        synchronized (mAggregatedNotifications) {
+            // if this notification still exists and has an autogroup overlay, but is now
+            // grouped by the app, clear the overlay
+            ArrayMap<String, NotificationAttributes> ungrouped =
+                mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            ungrouped.remove(sbn.getKey());
+            mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped);
+
+            final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            // check if the removed notification was part of the aggregate group
+            if (aggregatedNotificationsAttrs.containsKey(record.getKey())) {
+                aggregatedNotificationsAttrs.remove(sbn.getKey());
+                mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
+
+                if (DEBUG) {
+                    Log.i(TAG, "maybeUngroup removeAutoGroup: " + record);
+                }
+
+                mCallback.removeAutoGroup(sbn.getKey());
+
+                if (aggregatedNotificationsAttrs.isEmpty()) {
+                    if (DEBUG) {
+                        Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
+                    }
+                    mCallback.removeAutoGroupSummary(userId, pkgName,
+                            fullAggregateGroupKey.toString());
+                    mAggregatedNotifications.remove(fullAggregateGroupKey);
+                } else {
+                    if (DEBUG) {
+                        Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
+                    }
+                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
+                }
+            }
+        }
+    }
+
+    /**
+     * Called when a notification is newly posted, after some delay, so that the app
+     * has a chance to post a group summary or children (complete a group).
+     * Checks whether that notification and other active notifications should be forced grouped
+     * because their grouping is incorrect:
+     *  - missing summary
+     *  - only summaries
+     *  - sparse groups == multiple groups with very few notifications
+     *
+     * @param record the notification that was posted
+     * @param notificationList the full notification list from NotificationManagerService
+     * @param summaryByGroupKey the map of group summaries from NotificationManagerService
+     */
+    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+    protected void onNotificationPostedWithDelay(final NotificationRecord record,
+            final List<NotificationRecord> notificationList,
+            final Map<String, NotificationRecord> summaryByGroupKey) {
+        // Ungrouped notifications are handled separately in
+        // {@link #onNotificationPosted(StatusBarNotification, boolean)}
+        final StatusBarNotification sbn = record.getSbn();
+        if (!sbn.isAppGroup()) {
+            return;
+        }
+
+        if (record.isCanceled) {
+            return;
+        }
+
+        final NotificationSectioner sectioner = getSection(record);
+        if (sectioner == null) {
+            if (DEBUG) {
+                Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
+            }
+            return;
+        }
+
+        final String pkgName = sbn.getPackageName();
+        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
+                record.getUserId(), pkgName, sectioner);
+
+        // This notification is already aggregated
+        if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
+            return;
+        }
+
+        synchronized (mAggregatedNotifications) {
+            // scenario 1: group w/o summary
+            // scenario 2: summary w/o children
+            if (isGroupChildWithoutSummary(record, summaryByGroupKey) ||
+                isGroupSummaryWithoutChildren(record, notificationList)) {
+                if (DEBUG) {
+                    Log.i(TAG, "isGroupChildWithoutSummary OR isGroupSummaryWithoutChild"
+                            + record);
+                }
+
+                ArrayMap<String, NotificationAttributes> ungrouped =
+                        mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey,
+                            new ArrayMap<>());
+                ungrouped.put(record.getKey(), new NotificationAttributes(
+                    record.getFlags(),
+                    record.getNotification().getSmallIcon(),
+                    record.getNotification().color,
+                    record.getNotification().visibility,
+                    record.getNotification().getGroupAlertBehavior(),
+                    record.getChannel().getId()));
+                mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped);
+                // Create/update summary and group if >= mAutoGroupAtCount notifications
+                //  or if aggregate group exists
+                boolean hasSummary = !mAggregatedNotifications.getOrDefault(fullAggregateGroupKey,
+                    new ArrayMap<>()).isEmpty();
+                if (ungrouped.size() >= mAutoGroupAtCount || hasSummary) {
+                    if (DEBUG) {
+                        if (ungrouped.size() >= mAutoGroupAtCount) {
+                            Log.i(TAG,
+                                "Found >=" + mAutoGroupAtCount
+                                    + " ungrouped notifications => force grouping");
+                        } else {
+                            Log.i(TAG, "Found aggregate summary => force grouping");
+                        }
+                    }
+                    aggregateUngroupedNotifications(fullAggregateGroupKey, sbn.getKey(),
+                            ungrouped, hasSummary, sectioner.mSummaryId);
+                }
+
+                return;
+            }
+
+            // scenario 3: sparse/singleton groups
+            if (Flags.notificationForceGroupSingletons()) {
+                groupSparseGroups(record, notificationList, summaryByGroupKey, sectioner,
+                    fullAggregateGroupKey);
+            }
+        }
+    }
+
+    /**
+     * Called when a notification is removed, so that this helper can adjust the aggregate groups:
+     *  - Removes the autogroup summary of the notification's section
+     *      if the record was the last child.
+     *  - Recalculates the autogroup summary "attributes":
+     *      icon, icon color, visibility, groupAlertBehavior, flags - if the removed record was
+     *  part of an autogroup.
+     *  - Removes the saved summary of the original group, if the record was the last remaining
+     *      child of a sparse group that was forced auto-grouped.
+     *
+     * see also {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+     *
+     * @param record the removed notification
+     * @param notificationList the full notification list from NotificationManagerService
+     */
+    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+    protected void onNotificationRemoved(final NotificationRecord record,
+                final List<NotificationRecord> notificationList) {
+        final StatusBarNotification sbn = record.getSbn();
+        final String pkgName = sbn.getPackageName();
+        final int userId = record.getUserId();
+        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,
+                pkgName, getSection(record));
+
+        synchronized (mAggregatedNotifications) {
+            ArrayMap<String, NotificationAttributes> ungrouped =
+                mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            ungrouped.remove(record.getKey());
+            mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped);
+
+            final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            // check if the removed notification was part of the aggregate group
+            if (record.getGroupKey().equals(fullAggregateGroupKey.toString())
+                    || aggregatedNotificationsAttrs.containsKey(record.getKey())) {
+                aggregatedNotificationsAttrs.remove(record.getKey());
+                mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
+
+                if (aggregatedNotificationsAttrs.isEmpty()) {
+                    if (DEBUG) {
+                        Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
+                    }
+                    mCallback.removeAutoGroupSummary(userId, pkgName,
+                            fullAggregateGroupKey.toString());
+                    mAggregatedNotifications.remove(fullAggregateGroupKey);
+                } else {
+                    if (DEBUG) {
+                        Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
+                    }
+                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
+                }
+
+                // Try to cleanup cached summaries if notification was canceled (not snoozed)
+                if (record.isCanceled) {
+                    maybeClearCanceledSummariesCache(pkgName, userId,
+                            record.getNotification().getGroup(), notificationList);
+                }
+            }
+        }
+    }
+
+    private record NotificationMoveOp(NotificationRecord record, FullyQualifiedGroupKey oldGroup,
+                                      FullyQualifiedGroupKey newGroup) { }
+
+    /**
+     * Called when a notification channel is updated, so that this helper can adjust
+     * the aggregate groups by moving children if their section has changed.
+     * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+     * @param userId the userId of the channel
+     * @param pkgName the channel's package
+     * @param channel the channel that was updated
+     * @param notificationList the full notification list from NotificationManagerService
+     */
+    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void onChannelUpdated(final int userId, final String pkgName,
+            final NotificationChannel channel, final List<NotificationRecord> notificationList) {
+        synchronized (mAggregatedNotifications) {
+            ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
+            for (NotificationRecord r : notificationList) {
+                if (r.getChannel().getId().equals(channel.getId())
+                    && r.getSbn().getPackageName().equals(pkgName)
+                    && r.getUserId() == userId) {
+                    notificationsToCheck.put(r.getKey(), r);
+                }
+            }
+
+            final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
+
+            final Set<FullyQualifiedGroupKey> oldGroups =
+                    new HashSet<>(mAggregatedNotifications.keySet());
+            for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) {
+                // Only check aggregate groups that match the same userId & packageName
+                if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) {
+                    final ArrayMap<String, NotificationAttributes> notificationsInAggGroup =
+                            mAggregatedNotifications.get(oldFullAggKey);
+                    if (notificationsInAggGroup == null) {
+                        continue;
+                    }
+
+                    FullyQualifiedGroupKey newFullAggregateGroupKey = null;
+                    for (String key : notificationsInAggGroup.keySet()) {
+                        if (notificationsToCheck.get(key) != null) {
+                            // check if section changes
+                            NotificationSectioner sectioner = getSection(
+                                    notificationsToCheck.get(key));
+                            if (sectioner == null) {
+                                continue;
+                            }
+                            newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName,
+                                    sectioner);
+                            if (!oldFullAggKey.equals(newFullAggregateGroupKey)) {
+                                if (DEBUG) {
+                                    Log.i(TAG, "Change section on channel update: " + key);
+                                }
+                                notificationsToMove.add(
+                                        new NotificationMoveOp(notificationsToCheck.get(key),
+                                            oldFullAggKey, newFullAggregateGroupKey));
+                            }
+                        }
+                    }
+
+                    if (newFullAggregateGroupKey != null) {
+                        // Add any notifications left ungrouped to the new section
+                        ArrayMap<String, NotificationAttributes> ungrouped =
+                            mUngroupedAbuseNotifications.get(newFullAggregateGroupKey);
+                        if (ungrouped != null) {
+                            for (NotificationRecord r : notificationList) {
+                                if (ungrouped.containsKey(r.getKey())) {
+                                    if (DEBUG) {
+                                        Log.i(TAG, "Add previously ungrouped: " + r);
+                                    }
+                                    notificationsToMove.add(
+                                        new NotificationMoveOp(r, null, newFullAggregateGroupKey));
+                                }
+                            }
+                            //Cleanup mUngroupedAbuseNotifications
+                            mUngroupedAbuseNotifications.remove(newFullAggregateGroupKey);
+                        }
+                    }
+                }
+            }
+
+            // Batch move to new section
+            if (!notificationsToMove.isEmpty()) {
+                moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
+            }
+        }
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void moveNotificationsToNewSection(final int userId, final String pkgName,
+            final List<NotificationMoveOp> notificationsToMove) {
+        record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record,
+                             boolean hasSummary) { }
+        ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>();
+
+        for (NotificationMoveOp moveOp: notificationsToMove) {
+            final NotificationRecord record = moveOp.record;
+            final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup;
+            final FullyQualifiedGroupKey newFullAggregateGroupKey = moveOp.newGroup;
+
+            if (DEBUG) {
+                Log.i(TAG,
+                    "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey
+                        + " from: " + oldFullAggregateGroupKey);
+            }
+
+            // Update/remove aggregate summary for old group
+            if (oldFullAggregateGroupKey != null) {
+                final ArrayMap<String, NotificationAttributes> oldAggregatedNotificationsAttrs =
+                        mAggregatedNotifications.getOrDefault(oldFullAggregateGroupKey,
+                            new ArrayMap<>());
+                oldAggregatedNotificationsAttrs.remove(record.getKey());
+                mAggregatedNotifications.put(oldFullAggregateGroupKey,
+                        oldAggregatedNotificationsAttrs);
+
+                // Only add once, for triggering notification
+                if (!groupsToUpdate.containsKey(oldFullAggregateGroupKey)) {
+                    groupsToUpdate.put(oldFullAggregateGroupKey,
+                            new GroupUpdateOp(oldFullAggregateGroupKey, record, true));
+                }
+            }
+
+            // Add/update aggregate summary for new group
+            if (newFullAggregateGroupKey != null) {
+                final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs =
+                        mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey,
+                            new ArrayMap<>());
+                boolean newGroupExists = !newAggregatedNotificationsAttrs.isEmpty();
+                newAggregatedNotificationsAttrs.put(record.getKey(),
+                        new NotificationAttributes(record.getFlags(),
+                            record.getNotification().getSmallIcon(),
+                            record.getNotification().color,
+                            record.getNotification().visibility,
+                            record.getNotification().getGroupAlertBehavior(),
+                            record.getChannel().getId()));
+                mAggregatedNotifications.put(newFullAggregateGroupKey,
+                        newAggregatedNotificationsAttrs);
+
+                // Only add once, for triggering notification
+                if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) {
+                    groupsToUpdate.put(newFullAggregateGroupKey,
+                            new GroupUpdateOp(newFullAggregateGroupKey, record, newGroupExists));
+                }
+
+                // Add notification to new group. do not request resort
+                record.setOverrideGroupKey(null);
+                mCallback.addAutoGroup(record.getKey(), newFullAggregateGroupKey.toString(), false);
+            }
+        }
+
+        // Update groups (sections)
+        for (FullyQualifiedGroupKey groupKey : groupsToUpdate.keySet()) {
+            final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                    mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>());
+            if (aggregatedNotificationsAttrs.isEmpty()) {
+                mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString());
+                mAggregatedNotifications.remove(groupKey);
+            } else {
+                NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record;
+                boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary;
+                NotificationSectioner sectioner = getSection(triggeringNotification);
+                if (sectioner == null) {
+                    continue;
+                }
+                updateAggregateAppGroup(groupKey, triggeringNotification.getKey(), hasSummary,
+                        sectioner.mSummaryId);
+            }
+        }
+    }
+
+    static String getFullAggregateGroupKey(String pkgName,
+            String groupName, int userId) {
+        return new FullyQualifiedGroupKey(userId, pkgName, groupName).toString();
+    }
+
+    /**
+     * Returns the full aggregate group key, which contains the userId and package name
+     * in addition to the aggregate group key (name).
+     * Equivalent to {@link StatusBarNotification#groupKey()}
+     */
+    static String getFullAggregateGroupKey(NotificationRecord record) {
+        return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(),
+                getSection(record)).toString();
+    }
+
+    protected static boolean isAggregatedGroup(NotificationRecord record) {
+        return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0;
+    }
+
+    private static int getNumChildrenForGroup(@NonNull final String groupKey,
+            final List<NotificationRecord> notificationList) {
+        //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList
+        int numChildren = 0;
+        // find children for this summary
+        for (NotificationRecord r : notificationList) {
+            if (!r.getNotification().isGroupSummary()
+                    && groupKey.equals(r.getSbn().getGroup())) {
+                numChildren++;
+            }
+        }
+
+        if (DEBUG) {
+            Log.i(TAG, "getNumChildrenForGroup " + groupKey + " numChild: " + numChildren);
+        }
+        return numChildren;
+    }
+
+    private static boolean isGroupSummaryWithoutChildren(final NotificationRecord record,
+            final List<NotificationRecord> notificationList) {
+        final StatusBarNotification sbn = record.getSbn();
+        final String groupKey = record.getSbn().getGroup();
+
+        // ignore non app groups and non summaries
+        if (!sbn.isAppGroup() || !record.getNotification().isGroupSummary()) {
+            return false;
+        }
+
+        return getNumChildrenForGroup(groupKey, notificationList) == 0;
+    }
+
+    private static boolean isGroupChildWithoutSummary(final NotificationRecord record,
+            final Map<String, NotificationRecord> summaryByGroupKey) {
+        final StatusBarNotification sbn = record.getSbn();
+        final String groupKey = record.getSbn().getGroupKey();
+
+        if (!sbn.isAppGroup()) {
+            return false;
+        }
+
+        if (record.getNotification().isGroupSummary()) {
+            return false;
+        }
+
+        if (summaryByGroupKey.containsKey(groupKey)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void aggregateUngroupedNotifications(FullyQualifiedGroupKey fullAggregateGroupKey,
+            String triggeringNotifKey, Map<String, NotificationAttributes> ungrouped,
+            final boolean hasSummary, int summaryId) {
+        final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+        aggregatedNotificationsAttrs.putAll(ungrouped);
+        mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
+
+        // add/update aggregate summary
+        updateAggregateAppGroup(fullAggregateGroupKey, triggeringNotifKey, hasSummary, summaryId);
+
+        // add notification to aggregate group
+        for (String key: ungrouped.keySet()) {
+            mCallback.addAutoGroup(key, fullAggregateGroupKey.toString(), true);
+        }
+
+        //cleanup mUngroupedAbuseNotifications
+        mUngroupedAbuseNotifications.remove(fullAggregateGroupKey);
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void updateAggregateAppGroup(FullyQualifiedGroupKey fullAggregateGroupKey,
+            String triggeringNotifKey, boolean hasSummary, int summaryId) {
+        final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+        NotificationAttributes attr = getSummaryAttributes(fullAggregateGroupKey.pkg,
+                aggregatedNotificationsAttrs);
+        String channelId = hasSummary ? attr.channelId
+                : aggregatedNotificationsAttrs.get(triggeringNotifKey).channelId;
+        NotificationAttributes summaryAttr = new NotificationAttributes(attr.flags, attr.icon,
+                attr.iconColor, attr.visibility, attr.groupAlertBehavior, channelId);
+
+        if (!hasSummary) {
+            if (DEBUG) {
+                Log.i(TAG, "Create aggregate summary: " + fullAggregateGroupKey);
+            }
+            mCallback.addAutoGroupSummary(fullAggregateGroupKey.userId, fullAggregateGroupKey.pkg,
+                    triggeringNotifKey, fullAggregateGroupKey.toString(), summaryId, summaryAttr);
+        } else {
+            if (DEBUG) {
+                Log.i(TAG, "Update aggregate summary: " + fullAggregateGroupKey);
+            }
+            mCallback.updateAutogroupSummary(fullAggregateGroupKey.userId,
+                    fullAggregateGroupKey.pkg, fullAggregateGroupKey.toString(), summaryAttr);
+        }
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void groupSparseGroups(final NotificationRecord record,
+            final List<NotificationRecord> notificationList,
+            final Map<String, NotificationRecord> summaryByGroupKey,
+            final NotificationSectioner sectioner,
+            final FullyQualifiedGroupKey fullAggregateGroupKey) {
+        final ArrayMap<String, NotificationRecord> sparseGroupSummaries = getSparseGroups(
+                fullAggregateGroupKey, notificationList, summaryByGroupKey, sectioner);
+        if (sparseGroupSummaries.size() >= mAutogroupSparseGroupsAtCount) {
+            if (DEBUG) {
+                Log.i(TAG,
+                    "Aggregate sparse groups for: " + record.getSbn().getPackageName()
+                        + " Section: " + sectioner.mName);
+            }
+
+            ArrayMap<String, NotificationAttributes> ungrouped =
+                    mUngroupedAbuseNotifications.getOrDefault(
+                        fullAggregateGroupKey, new ArrayMap<>());
+            final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
+                    mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
+            final boolean hasSummary = !aggregatedNotificationsAttrs.isEmpty();
+            for (NotificationRecord r : notificationList) {
+                // Add notifications for detected sparse groups
+                if (sparseGroupSummaries.containsKey(r.getGroupKey())) {
+                    // Move child notifications to aggregate group
+                    if (!r.getNotification().isGroupSummary()) {
+                        if (DEBUG) {
+                            Log.i(TAG, "Aggregate notification (sparse group): " + r);
+                        }
+                        mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true);
+                        aggregatedNotificationsAttrs.put(r.getKey(),
+                            new NotificationAttributes(r.getFlags(),
+                                r.getNotification().getSmallIcon(), r.getNotification().color,
+                                r.getNotification().visibility,
+                                r.getNotification().getGroupAlertBehavior(),
+                                r.getChannel().getId()));
+
+                    } else if (r.getNotification().isGroupSummary()) {
+                        // Remove summary notifications
+                        if (DEBUG) {
+                            Log.i(TAG, "Remove app summary (sparse group): " + r);
+                        }
+                        mCallback.removeAppProvidedSummary(r.getKey());
+                        cacheCanceledSummary(r);
+                    }
+                } else {
+                    // Add any notifications left ungrouped
+                    if (ungrouped.containsKey(r.getKey())) {
+                        if (DEBUG) {
+                            Log.i(TAG, "Aggregate ungrouped (sparse group): " + r);
+                        }
+                        mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true);
+                        aggregatedNotificationsAttrs.put(r.getKey(),ungrouped.get(r.getKey()));
+                    }
+                }
+            }
+
+            mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
+            // add/update aggregate summary
+            updateAggregateAppGroup(fullAggregateGroupKey, record.getKey(), hasSummary,
+                    sectioner.mSummaryId);
+
+            //cleanup mUngroupedAbuseNotifications
+            mUngroupedAbuseNotifications.remove(fullAggregateGroupKey);
+        }
+    }
+
+    private ArrayMap<String, NotificationRecord> getSparseGroups(
+            final FullyQualifiedGroupKey fullAggregateGroupKey,
+            final List<NotificationRecord> notificationList,
+            final Map<String, NotificationRecord> summaryByGroupKey,
+            final NotificationSectioner sectioner) {
+        ArrayMap<String, NotificationRecord> sparseGroups = new ArrayMap<>();
+        for (NotificationRecord summary : summaryByGroupKey.values()) {
+            if (summary != null && sectioner.isInSection(summary)) {
+                if (summary.getSbn().getPackageName().equalsIgnoreCase(fullAggregateGroupKey.pkg)
+                        && summary.getUserId() == fullAggregateGroupKey.userId
+                        && summary.getSbn().isAppGroup()
+                        && !summary.getGroupKey().equals(fullAggregateGroupKey.toString())) {
+                    int numChildren = getNumChildrenForGroup(summary.getSbn().getGroup(),
+                            notificationList);
+                    if (numChildren > 0 && numChildren < MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING) {
+                        sparseGroups.put(summary.getGroupKey(), summary);
+                    }
+                }
+            }
+        }
+        return sparseGroups;
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void cacheCanceledSummary(NotificationRecord record) {
+        final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(),
+                record.getSbn().getPackageName(), record.getNotification().getGroup());
+        mCanceledSummaries.put(groupKey, new CachedSummary(record.getSbn().getId(),
+                record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey()));
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void maybeClearCanceledSummariesCache(String pkgName, int userId,
+            String groupName, List<NotificationRecord> notificationList) {
+        final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName,
+                groupName);
+        CachedSummary summary = mCanceledSummaries.get(findKey);
+        // Check if any notifications from original group remain
+        if (summary != null) {
+            if (DEBUG) {
+                Log.i(TAG, "Try removing cached summary: " + summary);
+            }
+            boolean stillHasChildren = false;
+            //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList
+            for (NotificationRecord r : notificationList) {
+                if (summary.originalGroupKey.equals(r.getNotification().getGroup())
+                    && r.getUser().getIdentifier() == userId
+                    && r.getSbn().getPackageName().equals(pkgName)) {
+                    stillHasChildren = true;
+                    break;
+                }
+            }
+            if (!stillHasChildren) {
+                removeCachedSummary(pkgName, userId, summary);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mAggregatedNotifications")
+    protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId) {
+        for (FullyQualifiedGroupKey key: mCanceledSummaries.keySet()) {
+            if (pkgName.equals(key.pkg) && userId == key.userId) {
+                CachedSummary summary = mCanceledSummaries.get(key);
+                if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) {
+                    return summary;
+                }
+            }
+        }
+        return null;
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mAggregatedNotifications")
+    protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId,
+            String groupName) {
+        final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName,
+                groupName);
+        CachedSummary summary = mCanceledSummaries.get(findKey);
+        if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) {
+            return summary;
+        } else {
+            return null;
+        }
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private void removeCachedSummary(String pkgName, int userId, CachedSummary summary) {
+        final FullyQualifiedGroupKey key = new FullyQualifiedGroupKey(userId, pkgName,
+                summary.originalGroupKey);
+        mCanceledSummaries.remove(key);
+    }
+
+    protected boolean isUpdateForCanceledSummary(final NotificationRecord record) {
+        synchronized (mAggregatedNotifications) {
+            if (record.getSbn().isAppGroup() && record.getNotification().isGroupSummary()) {
+                CachedSummary cachedSummary = findCanceledSummary(record.getSbn().getPackageName(),
+                        record.getSbn().getTag(), record.getSbn().getId(), record.getUserId(),
+                        record.getNotification().getGroup());
+                return cachedSummary != null;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Cancels the original group's children when an app cancels a summary that was 'maybe'
+     * previously removed due to forced grouping of a "sparse group".
+     *
+     * @param pkgName packageName
+     * @param tag original summary notification tag
+     * @param id original summary notification id
+     * @param userId original summary userId
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS)
+    public void maybeCancelGroupChildrenForCanceledSummary(String pkgName, String tag, int id,
+            int userId, int cancelReason) {
+        synchronized (mAggregatedNotifications) {
+            final CachedSummary summary = findCanceledSummary(pkgName, tag, id, userId);
+            if (summary != null) {
+                if (DEBUG) {
+                    Log.i(TAG, "Found cached summary: " + summary.key);
+                }
+                mCallback.removeNotificationFromCanceledGroup(userId, pkgName,
+                        summary.originalGroupKey, cancelReason);
+                removeCachedSummary(pkgName, userId, summary);
+            }
+        }
+    }
+
+    static NotificationSectioner getSection(final NotificationRecord record) {
+        for (NotificationSectioner sectioner: NOTIFICATION_SHADE_SECTIONS) {
+            if (sectioner.isInSection(record)) {
+                return sectioner;
+            }
+        }
+        return null;
+    }
+
+    record FullyQualifiedGroupKey(int userId, String pkg, String groupName) {
+        FullyQualifiedGroupKey(int userId, String pkg, @Nullable NotificationSectioner sectioner) {
+            this(userId, pkg, AGGREGATE_GROUP_KEY + (sectioner != null ? sectioner.mName : ""));
+        }
+
+        @Override
+        public String toString() {
+            return userId + "|" + pkg + "|" + "g:" + groupName;
+        }
+    }
+
+    protected static class NotificationSectioner {
+        final String mName;
+        final int mSummaryId;
+        private final Predicate<NotificationRecord> mSectionChecker;
+
+        public NotificationSectioner(String name, int summaryId,
+                Predicate<NotificationRecord> sectionChecker) {
+            mName = name;
+            mSummaryId = summaryId;
+            mSectionChecker = sectionChecker;
+        }
+
+        boolean isInSection(final NotificationRecord record) {
+            return isNotificationGroupable(record) && mSectionChecker.test(record);
+        }
+
+        private boolean isNotificationGroupable(final NotificationRecord record) {
+            if (record.isConversation()) {
+                return false;
+            }
+
+            Notification notification = record.getSbn().getNotification();
+            boolean isColorizedFGS = notification.isForegroundService()
+                && notification.isColorized()
+                && record.getImportance() > NotificationManager.IMPORTANCE_MIN;
+            boolean isCall = record.getImportance() > NotificationManager.IMPORTANCE_MIN
+                && notification.isStyle(Notification.CallStyle.class);
+            if (isColorizedFGS || isCall) {
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    record CachedSummary(int id, String tag, String originalGroupKey, String key) {}
+
     protected static class NotificationAttributes {
         public final int flags;
         public final int iconColor;
         public final Icon icon;
         public final int visibility;
+        public final int groupAlertBehavior;
+        public final String channelId;
 
-        public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility) {
+        public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility,
+                int groupAlertBehavior, String channelId) {
             this.flags = flags;
             this.icon = icon;
             this.iconColor = iconColor;
             this.visibility = visibility;
+            this.groupAlertBehavior = groupAlertBehavior;
+            this.channelId = channelId;
         }
 
         public NotificationAttributes(@NonNull NotificationAttributes attr) {
@@ -406,6 +1348,8 @@
             this.icon = attr.icon;
             this.iconColor = attr.iconColor;
             this.visibility = attr.visibility;
+            this.groupAlertBehavior = attr.groupAlertBehavior;
+            this.channelId = attr.channelId;
         }
 
         @Override
@@ -417,22 +1361,39 @@
                 return false;
             }
             return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon)
-                    && visibility == that.visibility;
+                    && visibility == that.visibility
+                    && groupAlertBehavior == that.groupAlertBehavior
+                    && channelId.equals(that.channelId);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(flags, iconColor, icon, visibility);
+            return Objects.hash(flags, iconColor, icon, visibility, groupAlertBehavior, channelId);
+        }
+
+        @Override
+        public String toString() {
+            return "NotificationAttributes: flags: " + flags + " icon: " + icon + " color: "
+                    + iconColor + " vis: " + visibility + " groupAlertBehavior: "
+                    + groupAlertBehavior + " channelId: " + channelId;
         }
     }
 
     protected interface Callback {
-        void addAutoGroup(String key, boolean requestSort);
+        void addAutoGroup(String key, String groupName, boolean requestSort);
         void removeAutoGroup(String key);
 
-        void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
+        void addAutoGroupSummary(int userId, String pkg, String triggeringKey, String groupName,
+                int summaryId, NotificationAttributes summaryAttr);
+        void removeAutoGroupSummary(int user, String pkg, String groupKey);
+
+        void updateAutogroupSummary(int userId, String pkg, String groupKey,
                 NotificationAttributes summaryAttr);
-        void removeAutoGroupSummary(int user, String pkg);
-        void updateAutogroupSummary(int userId, String pkg, NotificationAttributes summaryAttr);
+
+        // New callbacks for API abuse grouping
+        void removeAppProvidedSummary(String key);
+
+        void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey,
+                int cancelReason);
     }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index a4f534e..9e49827 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -39,6 +39,7 @@
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
 import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
+import static android.app.Notification.FLAG_GROUP_SUMMARY;
 import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
 import static android.app.Notification.FLAG_NO_CLEAR;
 import static android.app.Notification.FLAG_NO_DISMISS;
@@ -108,6 +109,7 @@
 import static android.service.notification.Adjustment.TYPE_PROMOTION;
 import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA;
 import static android.service.notification.Flags.callstyleCallbackApi;
+import static android.service.notification.Flags.notificationForceGrouping;
 import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners;
 import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -371,6 +373,7 @@
 import com.android.server.wm.BackgroundActivityStartCallback;
 import com.android.server.wm.WindowManagerInternal;
 
+import java.util.function.BiPredicate;
 import libcore.io.IoUtils;
 
 import org.json.JSONException;
@@ -512,6 +515,8 @@
 
     private static final long DELAY_FOR_ASSISTANT_TIME = 200;
 
+    private static final long DELAY_FORCE_REGROUP_TIME = 3000;
+
     private static final String ACTION_NOTIFICATION_TIMEOUT =
             NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
     private static final int REQUEST_CODE_TIMEOUT = 1;
@@ -608,6 +613,9 @@
 
     static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis();
 
+    // Minium number of sparse groups for a package before autogrouping them
+    private static final int AUTOGROUP_SPARSE_GROUPS_AT_COUNT = 3;
+
     private IActivityManager mAm;
     private ActivityTaskManagerInternal mAtm;
     private ActivityManager mActivityManager;
@@ -1001,17 +1009,25 @@
      * icons are different.
      * @param userId user id of the autogroup summary
      * @param pkg package of the autogroup summary
+     * @param groupKey group key of the autogroup summary
      * @param summaryAttr the new flags and/or icon & color for this summary
      * @param isAppForeground true if the app is currently in the foreground.
      */
     @GuardedBy("mNotificationLock")
-    protected void updateAutobundledSummaryLocked(int userId, String pkg,
-            NotificationAttributes summaryAttr, boolean isAppForeground) {
+    protected void updateAutobundledSummaryLocked(int userId, String pkg, String groupKey,
+                NotificationAttributes summaryAttr, boolean isAppForeground) {
         ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
         if (summaries == null) {
             return;
         }
-        String summaryKey = summaries.get(pkg);
+        final String autbundledGroupKey;
+        if (notificationForceGrouping()) {
+            autbundledGroupKey = groupKey;
+        } else {
+            autbundledGroupKey = pkg;
+        }
+
+        String summaryKey = summaries.get(autbundledGroupKey);
         if (summaryKey == null) {
             return;
         }
@@ -1019,12 +1035,26 @@
         if (summary == null) {
             return;
         }
+
         int oldFlags = summary.getSbn().getNotification().flags;
 
         boolean attributesUpdated =
                 !summaryAttr.icon.sameAs(summary.getSbn().getNotification().getSmallIcon())
                 || summaryAttr.iconColor != summary.getSbn().getNotification().color
-                || summaryAttr.visibility != summary.getSbn().getNotification().visibility;
+                || summaryAttr.visibility != summary.getSbn().getNotification().visibility
+                || summaryAttr.groupAlertBehavior !=
+                        summary.getSbn().getNotification().getGroupAlertBehavior();
+
+        if (notificationForceGrouping()) {
+            if (!summary.getChannel().getId().equals(summaryAttr.channelId)) {
+                NotificationChannel newChannel = mPreferencesHelper.getNotificationChannel(pkg,
+                        summary.getUid(), summaryAttr.channelId, false);
+                if (newChannel != null) {
+                    summary.updateNotificationChannel(newChannel);
+                    attributesUpdated = true;
+                }
+            }
+        }
 
         if (oldFlags != summaryAttr.flags || attributesUpdated) {
             summary.getSbn().getNotification().flags =
@@ -1032,6 +1062,8 @@
             summary.getSbn().getNotification().setSmallIcon(summaryAttr.icon);
             summary.getSbn().getNotification().color = summaryAttr.iconColor;
             summary.getSbn().getNotification().visibility = summaryAttr.visibility;
+            summary.getSbn().getNotification()
+                    .setGroupAlertBehavior(summaryAttr.groupAlertBehavior);
             mHandler.post(new EnqueueNotificationRunnable(userId, summary, isAppForeground,
                     /* isAppProvided= */ false, mPostNotificationTrackerFactory.newTracker(null)));
         }
@@ -2836,12 +2868,17 @@
         mAutoGroupAtCount =
                 getContext().getResources().getInteger(R.integer.config_autoGroupAtCount);
         return new GroupHelper(getContext(), getContext().getPackageManager(),
-                mAutoGroupAtCount, new GroupHelper.Callback() {
+                mAutoGroupAtCount, AUTOGROUP_SPARSE_GROUPS_AT_COUNT, new GroupHelper.Callback() {
             @Override
-            public void addAutoGroup(String key, boolean requestSort) {
-                        synchronized (mNotificationLock) {
-                            addAutogroupKeyLocked(key, requestSort);
-                        }
+            public void addAutoGroup(String key, String groupName, boolean requestSort) {
+                synchronized (mNotificationLock) {
+                    if (notificationForceGrouping()) {
+                        convertSummaryToNotificationLocked(key);
+                        addAutogroupKeyLocked(key, groupName, requestSort);
+                    } else {
+                        addAutogroupKeyLocked(key, groupName, requestSort);
+                    }
+                }
             }
 
             @Override
@@ -2853,10 +2890,9 @@
 
             @Override
             public void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
-                    NotificationAttributes summaryAttr) {
+                    String groupName, int summaryId, NotificationAttributes summaryAttr) {
                 NotificationRecord r = createAutoGroupSummary(userId, pkg, triggeringKey,
-                        summaryAttr.flags, summaryAttr.icon, summaryAttr.iconColor,
-                        summaryAttr.visibility);
+                        groupName, summaryId, summaryAttr);
                 if (r != null) {
                     final boolean isAppForeground =
                             mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
@@ -2867,19 +2903,56 @@
             }
 
             @Override
-            public void removeAutoGroupSummary(int userId, String pkg) {
+            public void removeAutoGroupSummary(int userId, String pkg, String groupKey) {
                 synchronized (mNotificationLock) {
-                    clearAutogroupSummaryLocked(userId, pkg);
+                    clearAutogroupSummaryLocked(userId, pkg, groupKey);
                 }
             }
 
             @Override
-            public void updateAutogroupSummary(int userId, String pkg,
+            public void updateAutogroupSummary(int userId, String pkg, String groupKey,
                     NotificationAttributes summaryAttr) {
                 boolean isAppForeground = pkg != null
                         && mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
                 synchronized (mNotificationLock) {
-                    updateAutobundledSummaryLocked(userId, pkg, summaryAttr, isAppForeground);
+                    updateAutobundledSummaryLocked(userId, pkg, groupKey, summaryAttr,
+                            isAppForeground);
+                }
+            }
+
+            @Override
+            public void removeAppProvidedSummary(String key) {
+                synchronized (mNotificationLock) {
+                    removeAppSummaryLocked(key);
+                }
+            }
+
+            @Override
+            public void removeNotificationFromCanceledGroup(int userId, String pkg,
+                    String groupKey, int cancelReason) {
+                synchronized (mNotificationLock) {
+                    final int mustNotHaveFlags;
+                    if (lifetimeExtensionRefactor()) {
+                        // Also don't allow client apps to cancel lifetime extended notifs.
+                        mustNotHaveFlags = (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
+                                | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+                    } else {
+                        mustNotHaveFlags = (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB);
+                    }
+                    FlagChecker childrenFlagChecker = (flags) -> {
+                            if (cancelReason == REASON_CANCEL
+                                || cancelReason == REASON_CLICK
+                                || cancelReason == REASON_CANCEL_ALL) {
+                                if ((flags & FLAG_BUBBLE) != 0) {
+                                    return false;
+                                }
+                            }
+                            return (flags & mustNotHaveFlags) == 0;
+                    };
+                    cancelGroupChildrenLocked(userId, pkg, Binder.getCallingUid(),
+                            Binder.getCallingPid(), null,
+                            false, childrenFlagChecker, groupKey,
+                            REASON_APP_CANCEL, SystemClock.elapsedRealtime());
                 }
             }
         });
@@ -3107,6 +3180,18 @@
                     modifiedChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
         }
 
+        if (notificationForceGrouping()) {
+            final NotificationChannel updatedChannel = mPreferencesHelper.getNotificationChannel(
+                    pkg, uid, channel.getId(), false);
+            mHandler.postDelayed(() -> {
+                synchronized (mNotificationLock) {
+                    mGroupHelper.onChannelUpdated(
+                            UserHandle.getUserHandleForUid(uid).getIdentifier(), pkg,
+                            updatedChannel, mNotificationList);
+                }
+            }, DELAY_FORCE_REGROUP_TIME);
+        }
+
         handleSavePolicyFile();
     }
 
@@ -6652,18 +6737,33 @@
         }
     }
 
+    @SuppressWarnings("GuardedBy")
     @GuardedBy("mNotificationLock")
-    void addAutogroupKeyLocked(String key, boolean requestSort) {
+    void addAutogroupKeyLocked(String key, String groupName, boolean requestSort) {
         NotificationRecord r = mNotificationsByKey.get(key);
         if (r == null) {
             return;
         }
         if (r.getSbn().getOverrideGroupKey() == null) {
-            addAutoGroupAdjustment(r, GroupHelper.AUTOGROUP_KEY);
+            if (notificationForceGrouping()) {
+                if (r.getSbn().isAppGroup()) {
+                    // Override group key early for forced grouped notifications
+                    r.setOverrideGroupKey(groupName);
+                }
+            }
+
+            addAutoGroupAdjustment(r, groupName);
             EventLogTags.writeNotificationAutogrouped(key);
+
             if (!android.app.Flags.checkAutogroupBeforePost() || requestSort) {
                 mRankingHandler.requestSort();
             }
+
+            if (notificationForceGrouping()) {
+                if (r.getSbn().isAppGroup()) {
+                    mListeners.notifyPostedLocked(r, r);
+                }
+            }
         }
     }
 
@@ -6692,27 +6792,57 @@
     // Clears the 'fake' auto-group summary.
     @VisibleForTesting
     @GuardedBy("mNotificationLock")
-    void clearAutogroupSummaryLocked(int userId, String pkg) {
+    void clearAutogroupSummaryLocked(int userId, String pkg, String groupKey) {
+        final String autbundledGroupKey;
+        if (notificationForceGrouping()) {
+            autbundledGroupKey = groupKey;
+        } else {
+            autbundledGroupKey = pkg;
+        }
         ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
-        if (summaries != null && summaries.containsKey(pkg)) {
-            final NotificationRecord removed = findNotificationByKeyLocked(summaries.remove(pkg));
+        if (summaries != null && summaries.containsKey(autbundledGroupKey)) {
+            final NotificationRecord removed = findNotificationByKeyLocked(
+                    summaries.remove(autbundledGroupKey));
             if (removed != null) {
                 final StatusBarNotification sbn = removed.getSbn();
                 cancelNotification(MY_UID, MY_PID, pkg, sbn.getTag(), sbn.getId(), 0, 0, false,
-                        userId, REASON_UNAUTOBUNDLED, null);
+                    userId, REASON_UNAUTOBUNDLED, null);
             }
         }
     }
 
     @GuardedBy("mNotificationLock")
-    private boolean hasAutoGroupSummaryLocked(StatusBarNotification sbn) {
-        ArrayMap<String, String> summaries = mAutobundledSummaries.get(sbn.getUserId());
-        return summaries != null && summaries.containsKey(sbn.getPackageName());
+    void removeAppSummaryLocked(String key) {
+        NotificationRecord r = mNotificationsByKey.get(key);
+        if (r == null) {
+            return;
+        }
+        if (convertSummaryToNotificationLocked(key)) {
+            r.isCanceled = true;
+            cancelNotification(Binder.getCallingUid(),
+                    Binder.getCallingPid(), r.getSbn().getPackageName(),
+                    r.getSbn().getTag(), r.getSbn().getId(), 0, 0,
+                    false, r.getUserId(),
+                    NotificationListenerService.REASON_GROUP_OPTIMIZATION, null);
+        }
+    }
+
+    @GuardedBy("mNotificationLock")
+    private boolean hasAutoGroupSummaryLocked(NotificationRecord record) {
+        final String autbundledGroupKey;
+        if (notificationForceGrouping()) {
+            autbundledGroupKey = GroupHelper.getFullAggregateGroupKey(record);
+        } else {
+            autbundledGroupKey = record.getSbn().getPackageName();
+        }
+
+        ArrayMap<String, String> summaries = mAutobundledSummaries.get(record.getUserId());
+        return summaries != null && summaries.containsKey(autbundledGroupKey);
     }
 
     // Creates a 'fake' summary for a package that has exceeded the solo-notification limit.
     NotificationRecord createAutoGroupSummary(int userId, String pkg, String triggeringKey,
-            int flagsToSet, Icon summaryIcon, int summaryIconColor, int summaryVisibilty) {
+            String groupKey, int summaryId, NotificationAttributes summaryAttr) {
         NotificationRecord summaryRecord = null;
         boolean isPermissionFixed = mPermissionHelper.isPermissionFixed(pkg, userId);
         synchronized (mNotificationLock) {
@@ -6730,24 +6860,35 @@
                 summaries = new ArrayMap<>();
             }
             mAutobundledSummaries.put(userId, summaries);
-            if (!summaries.containsKey(pkg)) {
+
+            boolean hasSummary;
+            String channelId;
+            if (notificationForceGrouping()) {
+                hasSummary = summaries.containsKey(groupKey);
+                channelId = summaryAttr.channelId;
+            } else {
+                hasSummary = summaries.containsKey(pkg);
+                channelId = notificationRecord.getChannel().getId();
+            }
+
+            if (!hasSummary) {
                 // Add summary
                 final ApplicationInfo appInfo =
                         adjustedSbn.getNotification().extras.getParcelable(
                                 EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class);
                 final Bundle extras = new Bundle();
                 extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, appInfo);
-                final String channelId = notificationRecord.getChannel().getId();
+
 
                 final Notification summaryNotification =
                                 new Notification.Builder(getContext(), channelId)
-                                .setSmallIcon(summaryIcon)
+                                .setSmallIcon(summaryAttr.icon)
                                 .setGroupSummary(true)
-                                .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN)
-                                .setGroup(GroupHelper.AUTOGROUP_KEY)
-                                .setFlag(flagsToSet, true)
-                                .setColor(summaryIconColor)
-                                .setVisibility(summaryVisibilty)
+                                .setGroupAlertBehavior(summaryAttr.groupAlertBehavior)
+                                .setGroup(groupKey)
+                                .setFlag(summaryAttr.flags, true)
+                                .setColor(summaryAttr.iconColor)
+                                .setVisibility(summaryAttr.visibility)
                                 .build();
                 summaryNotification.extras.putAll(extras);
                 Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
@@ -6759,17 +6900,22 @@
                 final StatusBarNotification summarySbn =
                         new StatusBarNotification(adjustedSbn.getPackageName(),
                                 adjustedSbn.getOpPkg(),
-                                Integer.MAX_VALUE,
-                                GroupHelper.AUTOGROUP_KEY, adjustedSbn.getUid(),
+                                summaryId,
+                                groupKey, adjustedSbn.getUid(),
                                 adjustedSbn.getInitialPid(), summaryNotification,
-                                adjustedSbn.getUser(), GroupHelper.AUTOGROUP_KEY,
+                                adjustedSbn.getUser(), groupKey,
                                 System.currentTimeMillis());
                 summaryRecord = new NotificationRecord(getContext(), summarySbn,
                         notificationRecord.getChannel());
                 summaryRecord.setImportanceFixed(isPermissionFixed);
                 summaryRecord.setIsAppImportanceLocked(
                         notificationRecord.getIsAppImportanceLocked());
-                summaries.put(pkg, summarySbn.getKey());
+
+                if (notificationForceGrouping()) {
+                    summaries.put(summarySbn.getGroupKey(), summarySbn.getKey());
+                } else {
+                    summaries.put(pkg, summarySbn.getKey());
+                }
             }
             if (summaryRecord != null && checkDisqualifyingFeatures(userId, uid,
                     summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord,
@@ -6780,6 +6926,27 @@
         return null;
     }
 
+    @GuardedBy("mNotificationLock")
+    boolean convertSummaryToNotificationLocked(final String key) {
+        NotificationRecord r = mNotificationsByKey.get(key);
+        if (r == null) {
+            return false;
+        }
+        // Convert summary to regular notification
+        if (r.getSbn().isAppGroup() && r.getNotification().isGroupSummary()) {
+            String oldGroupKey = r.getGroupKey();
+            NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey);
+            if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) {
+                mSummaryByGroupKey.remove(oldGroupKey);
+            }
+            // Clear summary flag
+            StatusBarNotification sbn = r.getSbn();
+            sbn.getNotification().flags = (r.mOriginalFlags & ~FLAG_GROUP_SUMMARY);
+            return true;
+        }
+        return false;
+    }
+
     // Gets packages that have requested notification permission, and whether that has been
     // allowed/denied, for all users on the device.
     // Returns a single map containing that info keyed by (uid, package name) for all users.
@@ -8347,8 +8514,15 @@
          * They will be recreated as needed when the group children are unsnoozed
          */
         private boolean isSnoozable(NotificationRecord record) {
-            return !(record.getNotification().isGroupSummary() && GroupHelper.AUTOGROUP_KEY.equals(
-                    record.getNotification().getGroup()));
+            if (notificationForceGrouping()) {
+                boolean isExemptedSummary =
+                        ((record.getFlags() & FLAG_AUTOGROUP_SUMMARY) != 0
+                        || GroupHelper.isAggregatedGroup(record));
+                return !(record.getNotification().isGroupSummary() && isExemptedSummary);
+            } else {
+                return !(record.getNotification().isGroupSummary()
+                        && GroupHelper.AUTOGROUP_KEY.equals(record.getNotification().getGroup()));
+            }
         }
     }
 
@@ -8471,9 +8645,12 @@
                     cancelNotificationLocked(
                             r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName,
                             mCancellationElapsedTimeMs);
-                    cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
-                            mSendDelete, childrenFlagChecker, mReason,
-                            mCancellationElapsedTimeMs);
+                    if (r.getNotification().isGroupSummary()) {
+                        cancelGroupChildrenLocked(mUserId, mPkg, mCallingUid, mCallingPid,
+                                listenerName, mSendDelete, childrenFlagChecker,
+                                r.getNotification().getGroup(), mReason,
+                                mCancellationElapsedTimeMs);
+                    }
                     mAttentionHelper.updateLightsLocked();
                     if (mShortcutHelper != null) {
                         mShortcutHelper.maybeListenForShortcutChangesForBubbles(r,
@@ -8481,6 +8658,14 @@
                                 mHandler);
                     }
                 } else {
+                    if (notificationForceGrouping()) {
+                        // No notification was found => maybe it was canceled by forced grouping
+                        if (Flags.notificationForceGroupSingletons()) {
+                            mGroupHelper.maybeCancelGroupChildrenForCanceledSummary(mPkg, mTag,
+                                mId, mUserId, mReason);
+                        }
+                    }
+
                     // No notification was found, assume that it is snoozed and cancel it.
                     if (mReason != REASON_SNOOZED) {
                         final boolean wasSnoozed = mSnoozeHelper.cancel(mUserId, mPkg, mTag, mId);
@@ -8708,7 +8893,7 @@
             boolean appBanned = !areNotificationsEnabledForPackageInt(pkg, uid);
             boolean isCallNotification = isCallNotification(pkg, uid);
             boolean posted = false;
-            synchronized (mNotificationLock) {
+            synchronized (NotificationManagerService.this.mNotificationLock) {
                 try {
                     NotificationRecord r = findNotificationByListLocked(mEnqueuedNotifications,
                             key);
@@ -8731,6 +8916,29 @@
                         return false;
                     }
 
+                    if (notificationForceGrouping()) {
+                        if (Flags.notificationForceGroupSingletons()) {
+                            // Check if this is an updated for a summary for an aggregated sparse
+                            // group and remove it because that summary has been canceled
+                            if (mGroupHelper.isUpdateForCanceledSummary(r)) {
+                                if (DBG) {
+                                    Log.w(TAG,
+                                        "Suppressing notification because summary was canceled: "
+                                            + r);
+                                }
+
+                                String groupKey = r.getGroupKey();
+                                NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey);
+                                if (groupSummary != null && groupSummary.getKey()
+                                        .equals(r.getKey())) {
+                                    mSummaryByGroupKey.remove(groupKey);
+                                }
+                                return false;
+                            }
+                        }
+                    }
+
+
                     final boolean isPackageSuspended =
                             isPackagePausedOrSuspended(r.getSbn().getPackageName(), r.getUid());
                     r.setHidden(isPackageSuspended);
@@ -8788,18 +8996,38 @@
                         if (notification.getSmallIcon() != null && !isCritical(r)) {
                             StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
                             if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())
+                                    || !Objects.equals(oldSbn.getNotification().getGroup(),
+                                        n.getNotification().getGroup())
                                     || oldSbn.getNotification().flags
                                     != n.getNotification().flags) {
                                 synchronized (mNotificationLock) {
-                                    boolean willBeAutogrouped = mGroupHelper.onNotificationPosted(n,
-                                            hasAutoGroupSummaryLocked(n));
+                                    final String autogroupName =
+                                            notificationForceGrouping() ?
+                                                GroupHelper.getFullAggregateGroupKey(r)
+                                                : GroupHelper.AUTOGROUP_KEY;
+                                    boolean willBeAutogrouped =
+                                            mGroupHelper.onNotificationPosted(r,
+                                                hasAutoGroupSummaryLocked(r));
                                     if (willBeAutogrouped) {
                                         // The newly posted notification will be autogrouped, but
                                         // was not autogrouped onPost, to avoid an unnecessary sort.
                                         // We add the autogroup key to the notification without a
                                         // sort here, and it'll be sorted below with extractSignals.
-                                        addAutogroupKeyLocked(key, /* requestSort= */false);
+                                        addAutogroupKeyLocked(key,
+                                                autogroupName, /*requestSort=*/false);
+                                    } else {
+                                        if (notificationForceGrouping()) {
+                                            // Wait 3 seconds so that the app has a chance to post
+                                            // a group summary or children (complete a group)
+                                            mHandler.postDelayed(() -> {
+                                                synchronized (mNotificationLock) {
+                                                    mGroupHelper.onNotificationPostedWithDelay(
+                                                        r, mNotificationList, mSummaryByGroupKey);
+                                                }
+                                            }, r.getKey(), DELAY_FORCE_REGROUP_TIME);
+                                        }
                                     }
+
                                 }
                             }
                         }
@@ -8835,9 +9063,18 @@
                                     mHandler.post(() -> {
                                         synchronized (mNotificationLock) {
                                             mGroupHelper.onNotificationPosted(
-                                                    n, hasAutoGroupSummaryLocked(n));
+                                                    r, hasAutoGroupSummaryLocked(r));
                                         }
                                     });
+
+                                    if (notificationForceGrouping()) {
+                                        mHandler.postDelayed(() -> {
+                                            synchronized (mNotificationLock) {
+                                                mGroupHelper.onNotificationPostedWithDelay(r,
+                                                        mNotificationList, mSummaryByGroupKey);
+                                            }
+                                        }, r.getKey(), DELAY_FORCE_REGROUP_TIME);
+                                    }
                                 }
                             }
                         }
@@ -8846,12 +9083,20 @@
                         if (old != null && !old.isCanceled) {
                             mListeners.notifyRemovedLocked(r,
                                     REASON_ERROR, r.getStats());
-                            mHandler.post(new Runnable() {
-                                @Override
-                                public void run() {
-                                    mGroupHelper.onNotificationRemoved(n);
-                                }
-                            });
+                            if (notificationForceGrouping()) {
+                                mHandler.post(() -> {
+                                    synchronized (mNotificationLock) {
+                                        mGroupHelper.onNotificationRemoved(r, mNotificationList);
+                                    }
+                                });
+                            } else {
+                                mHandler.post(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mGroupHelper.onNotificationRemoved(r);
+                                    }
+                                });
+                            }
                         }
 
                         if (callstyleCallbackApi()) {
@@ -9082,6 +9327,18 @@
             n.flags &= ~Notification.FLAG_GROUP_SUMMARY;
         }
 
+        if (notificationForceGrouping()) {
+            if (old != null) {
+                // If this is an update to a summary that was forced grouped => remove summary flag
+                boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0;
+                boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0
+                        && old.getSbn().getOverrideGroupKey() != null;
+                if (n.isGroupSummary() && wasSummary && wasForcedGrouped) {
+                    n.flags &= ~FLAG_GROUP_SUMMARY;
+                }
+            }
+        }
+
         String group = sbn.getGroupKey();
         boolean isSummary = n.isGroupSummary();
 
@@ -9114,8 +9371,10 @@
         // notification was a summary and the new one isn't, or when the old
         // notification was a summary and its group key changed.
         if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
-            cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */,
-                    childrenFlagChecker, REASON_APP_CANCEL, SystemClock.elapsedRealtime());
+            cancelGroupChildrenLocked(old.getUserId(), old.getSbn().getPackageName(), callingUid,
+                    callingPid, null, false /* sendDelete */, childrenFlagChecker,
+                    old.getNotification().getGroup(), REASON_APP_CANCEL,
+                    SystemClock.elapsedRealtime());
         }
     }
 
@@ -9777,12 +10036,21 @@
                     r.isCanceled = true;
                 }
                 mListeners.notifyRemovedLocked(r, reason, r.getStats());
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        mGroupHelper.onNotificationRemoved(r.getSbn());
-                    }
-                });
+                if (notificationForceGrouping()) {
+                    mHandler.removeCallbacksAndMessages(r.getKey());
+                    mHandler.post(() -> {
+                        synchronized (NotificationManagerService.this.mNotificationLock) {
+                            mGroupHelper.onNotificationRemoved(r, mNotificationList);
+                        }
+                    });
+                } else {
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mGroupHelper.onNotificationRemoved(r);
+                        }
+                    });
+                }
                 if (callstyleCallbackApi()) {
                     notifyCallNotificationEventListenerOnRemoved(r);
                 }
@@ -9815,9 +10083,15 @@
         }
         final ArrayMap<String, String> summaries =
                 mAutobundledSummaries.get(r.getSbn().getUserId());
+        final String autbundledGroupKey;
+        if (notificationForceGrouping()) {
+            autbundledGroupKey = groupKey;
+        } else {
+            autbundledGroupKey = r.getSbn().getPackageName();
+        }
         if (summaries != null && r.getSbn().getKey().equals(
-                summaries.get(r.getSbn().getPackageName()))) {
-            summaries.remove(r.getSbn().getPackageName());
+                summaries.get(autbundledGroupKey))) {
+            summaries.remove(autbundledGroupKey);
         }
 
         // Save it for users of getHistoricalNotifications(), unless the whole channel was deleted
@@ -10081,6 +10355,15 @@
         public boolean apply(int flags);
     }
 
+    private static boolean isChildOfGroup(final NotificationRecord childRecord, int userId,
+            String pkg, String groupKey) {
+        return (childRecord.getUser().getIdentifier() == userId
+            && childRecord.getSbn().getPackageName().equals(pkg)
+            && childRecord.getSbn().isGroup()
+            && !childRecord.getNotification().isGroupSummary()
+            && TextUtils.equals(groupKey, childRecord.getNotification().getGroup()));
+    }
+
     @GuardedBy("mNotificationLock")
     private void cancelAllNotificationsByListLocked(ArrayList<NotificationRecord> notificationList,
             @Nullable String pkg, boolean nullPkgIndicatesUserSwitch, @Nullable String channelId,
@@ -10238,43 +10521,34 @@
 
     // Warning: The caller is responsible for invoking updateLightsLocked().
     @GuardedBy("mNotificationLock")
-    private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid,
-            String listenerName, boolean sendDelete, FlagChecker flagChecker, int reason,
-            @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
-        Notification n = r.getNotification();
-        if (!n.isGroupSummary()) {
-            return;
-        }
-
-        String pkg = r.getSbn().getPackageName();
-
+    private void cancelGroupChildrenLocked(int userId, String pkg, int callingUid, int callingPid,
+            String listenerName, boolean sendDelete, FlagChecker flagChecker, String groupKey,
+            int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
         if (pkg == null) {
-            if (DBG) Slog.e(TAG, "No package for group summary: " + r.getKey());
+            if (DBG) Slog.e(TAG, "No package for group summary");
             return;
         }
 
-        cancelGroupChildrenByListLocked(mNotificationList, r, callingUid, callingPid, listenerName,
-                sendDelete, true, flagChecker, reason, cancellationElapsedTimeMs);
-        cancelGroupChildrenByListLocked(mEnqueuedNotifications, r, callingUid, callingPid,
-                listenerName, sendDelete, false, flagChecker, reason, cancellationElapsedTimeMs);
+        cancelGroupChildrenByListLocked(mNotificationList, userId, pkg, callingUid, callingPid,
+                listenerName, sendDelete, true, flagChecker, groupKey,
+                reason, cancellationElapsedTimeMs);
+        cancelGroupChildrenByListLocked(mEnqueuedNotifications, userId, pkg, callingUid, callingPid,
+                listenerName, sendDelete, false, flagChecker, groupKey,
+                reason, cancellationElapsedTimeMs);
     }
 
     @GuardedBy("mNotificationLock")
     private void cancelGroupChildrenByListLocked(ArrayList<NotificationRecord> notificationList,
-            NotificationRecord parentNotification, int callingUid, int callingPid,
+            int userId, String pkg, int callingUid, int callingPid,
             String listenerName, boolean sendDelete, boolean wasPosted, FlagChecker flagChecker,
-            int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
-        final String pkg = parentNotification.getSbn().getPackageName();
-        final int userId = parentNotification.getUserId();
+            String groupKey, int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
         final int childReason = REASON_GROUP_SUMMARY_CANCELED;
         for (int i = notificationList.size() - 1; i >= 0; i--) {
             final NotificationRecord childR = notificationList.get(i);
             final StatusBarNotification childSbn = childR.getSbn();
-            if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) &&
-                    childR.getGroupKey().equals(parentNotification.getGroupKey())
-                    && (flagChecker == null || flagChecker.apply(childR.getFlags()))
-                    && (!childR.getChannel().isImportantConversation()
-                            || reason != REASON_CANCEL)) {
+            if (isChildOfGroup(childR, userId, pkg, groupKey)
+                && (flagChecker == null || flagChecker.apply(childR.getFlags()))
+                && (!childR.getChannel().isImportantConversation() || reason != REASON_CANCEL)) {
                 EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(),
                         childSbn.getTag(), userId, 0, 0, childReason, listenerName);
                 notificationList.remove(i);
@@ -10354,6 +10628,7 @@
                 != null) {
             return r;
         }
+
         return null;
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 0d4bdf6..bd00901 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -449,9 +449,16 @@
         mRankingTimeMs = calculateRankingTimeMs(previous.getRankingTimeMs());
         mCreationTimeMs = previous.mCreationTimeMs;
         mVisibleSinceMs = previous.mVisibleSinceMs;
-        if (previous.getSbn().getOverrideGroupKey() != null && !getSbn().isAppGroup()) {
-            getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey());
+        if (android.service.notification.Flags.notificationForceGrouping()) {
+            if (previous.getSbn().getOverrideGroupKey() != null) {
+                getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey());
+            }
+        } else {
+            if (previous.getSbn().getOverrideGroupKey() != null && !getSbn().isAppGroup()) {
+                getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey());
+            }
         }
+
         // Don't copy importance information or mGlobalSortKey, recompute them.
     }
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index bf6b652..7265cff 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -141,4 +141,11 @@
   namespace: "systemui"
   description: "This flag does not allow notifications older than 2 weeks old to be posted"
   bug: "339833083"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "notification_force_group_singletons"
+  namespace: "systemui"
+  description: "This flag enables forced auto-grouping singleton groups"
+  bug: "336488844"
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 8a7d276..225c1dc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -20,13 +20,23 @@
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.Notification.FLAG_CAN_COLORIZE;
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
+import static android.app.Notification.FLAG_GROUP_SUMMARY;
 import static android.app.Notification.FLAG_NO_CLEAR;
 import static android.app.Notification.FLAG_ONGOING_EVENT;
+import static android.app.Notification.GROUP_ALERT_ALL;
+import static android.app.Notification.GROUP_ALERT_CHILDREN;
+import static android.app.Notification.GROUP_ALERT_SUMMARY;
 import static android.app.Notification.VISIBILITY_PRIVATE;
 import static android.app.Notification.VISIBILITY_PUBLIC;
 import static android.app.Notification.VISIBILITY_SECRET;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
+import static com.android.server.notification.GroupHelper.AGGREGATE_GROUP_KEY;
+import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY;
 import static com.android.server.notification.GroupHelper.BASE_FLAGS;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -41,6 +51,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -49,6 +60,7 @@
 
 import android.annotation.SuppressLint;
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.content.pm.PackageManager;
 import android.graphics.Color;
 import android.graphics.drawable.AdaptiveIconDrawable;
@@ -66,6 +78,7 @@
 
 import com.android.internal.R;
 import com.android.server.UiServiceTestCase;
+import com.android.server.notification.GroupHelper.CachedSummary;
 import com.android.server.notification.GroupHelper.NotificationAttributes;
 
 import org.junit.Before;
@@ -90,11 +103,15 @@
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
     private final int DEFAULT_VISIBILITY = VISIBILITY_PRIVATE;
+    private final int DEFAULT_GROUP_ALERT = GROUP_ALERT_CHILDREN;
+
+    private final String TEST_CHANNEL_ID = "TEST_CHANNEL_ID";
 
     private @Mock GroupHelper.Callback mCallback;
     private @Mock PackageManager mPackageManager;
 
     private final static int AUTOGROUP_AT_COUNT = 7;
+    private final static int AUTOGROUP_SINGLETONS_AT_COUNT = 2;
     private GroupHelper mGroupHelper;
     private @Mock Icon mSmallIcon;
 
@@ -113,7 +130,7 @@
         MockitoAnnotations.initMocks(this);
 
         mGroupHelper = new GroupHelper(getContext(), mPackageManager, AUTOGROUP_AT_COUNT,
-                mCallback);
+                AUTOGROUP_SINGLETONS_AT_COUNT, mCallback);
 
         NotificationRecord r = mock(NotificationRecord.class);
         StatusBarNotification sbn = getSbn("package", 0, "0", UserHandle.SYSTEM);
@@ -124,7 +141,7 @@
 
     private StatusBarNotification getSbn(String pkg, int id, String tag,
             UserHandle user, String groupKey, Icon smallIcon, int iconColor) {
-        Notification.Builder nb = new Notification.Builder(getContext(), "test_channel_id")
+        Notification.Builder nb = new Notification.Builder(getContext(), TEST_CHANNEL_ID)
                 .setContentTitle("A")
                 .setWhen(1205)
                 .setSmallIcon(smallIcon)
@@ -146,15 +163,54 @@
         return getSbn(pkg, id, tag, user, null);
     }
 
+    private NotificationRecord getNotificationRecord(String pkg, int id, String tag,
+            UserHandle user) {
+        return getNotificationRecord(pkg, id, tag, user, null, false);
+    }
+
+    private NotificationRecord getNotificationRecord(String pkg, int id, String tag,
+            UserHandle user, String groupKey, boolean isSummary) {
+        return getNotificationRecord(pkg, id, tag, user, groupKey, isSummary, IMPORTANCE_DEFAULT);
+    }
+
+    private NotificationRecord getNotificationRecord(String pkg, int id, String tag,
+            UserHandle user, String groupKey, boolean isSummary, int importance) {
+        return getNotificationRecord(pkg, id, tag, user, groupKey, isSummary,
+                new NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_ID, importance));
+    }
+
+    private NotificationRecord getNotificationRecord(String pkg, int id, String tag,
+            UserHandle user, String groupKey, boolean isSummary, NotificationChannel channel) {
+        StatusBarNotification sbn = getSbn(pkg, id, tag, user, groupKey);
+        if (isSummary) {
+            sbn.getNotification().flags |= FLAG_GROUP_SUMMARY;
+        }
+        return new NotificationRecord(getContext(), sbn, channel);
+    }
+
+    private NotificationRecord getNotificationRecord(StatusBarNotification sbn) {
+        return new NotificationRecord(getContext(), sbn,
+            new NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT));
+    }
+
     private NotificationAttributes getNotificationAttributes(int flags) {
-        return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY);
+        return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY,
+                DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+    }
+
+    private String getExpectedAutogroupKey(final NotificationRecord record) {
+        if (android.service.notification.Flags.notificationForceGrouping()) {
+            return GroupHelper.getFullAggregateGroupKey(record);
+        } else {
+            return AUTOGROUP_KEY;
+        }
     }
 
     @Test
     public void testGetAutogroupSummaryFlags_noChildren() {
         ArrayMap<String, NotificationAttributes> children = new ArrayMap<>();
 
-        assertEquals(BASE_FLAGS, mGroupHelper.getAutogroupSummaryFlags(children));
+        assertEquals(BASE_FLAGS, GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -165,7 +221,7 @@
         children.put("c", getNotificationAttributes(FLAG_BUBBLE));
 
         assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -176,7 +232,7 @@
         children.put("c", getNotificationAttributes(FLAG_BUBBLE));
 
         assertEquals(FLAG_NO_CLEAR | FLAG_ONGOING_EVENT | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -187,7 +243,7 @@
         children.put("c", getNotificationAttributes(FLAG_BUBBLE));
 
         assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -199,7 +255,7 @@
         children.put("d", getNotificationAttributes(FLAG_ONGOING_EVENT));
 
         assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -210,7 +266,7 @@
         children.put("c", getNotificationAttributes(FLAG_BUBBLE));
 
         assertEquals(BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -222,7 +278,7 @@
         children.put("d", getNotificationAttributes(FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE));
 
         assertEquals(FLAG_AUTO_CANCEL | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
@@ -235,15 +291,16 @@
                 FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE | FLAG_ONGOING_EVENT));
 
         assertEquals(FLAG_AUTO_CANCEL| FLAG_ONGOING_EVENT | BASE_FLAGS,
-                mGroupHelper.getAutogroupSummaryFlags(children));
+                GroupHelper.getAutogroupSummaryFlags(children));
     }
 
     @Test
     public void testNoGroup_postingUnderLimit() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
-            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
-                    false);
+            mGroupHelper.onNotificationPosted(
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
+                false);
         }
         verifyZeroInteractions(mCallback);
     }
@@ -253,11 +310,12 @@
         final String pkg = "package";
         final String pkg2 = "package2";
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
-            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
-                    false);
+            mGroupHelper.onNotificationPosted(
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
+                false);
         }
         mGroupHelper.onNotificationPosted(
-                getSbn(pkg2, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM), false);
+            getNotificationRecord(pkg2, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM), false);
         verifyZeroInteractions(mCallback);
     }
 
@@ -265,11 +323,12 @@
     public void testNoGroup_multiUser() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
-            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
-                    false);
+            mGroupHelper.onNotificationPosted(
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
+                false);
         }
         mGroupHelper.onNotificationPosted(
-                getSbn(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.of(7)), false);
+            getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.of(7)), false);
         verifyZeroInteractions(mCallback);
     }
 
@@ -278,10 +337,11 @@
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
             mGroupHelper.onNotificationPosted(
-                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
         }
         mGroupHelper.onNotificationPosted(
-                getSbn(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a"), false);
+            getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a", false),
+            false);
         verifyZeroInteractions(mCallback);
     }
 
@@ -289,185 +349,241 @@
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_alwaysAutogroup() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             mGroupHelper.onNotificationPosted(
-                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+                anyInt(), eq(pkg), anyString(), eq(autogroupKey),
+                anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
             assertThat(mGroupHelper.onNotificationPosted(
-                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false)).isFalse();
+                    getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM),
+                    false)).isFalse();
         }
         assertThat(mGroupHelper.onNotificationPosted(
-                getSbn(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1),
+            getNotificationRecord(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1),
                         UserHandle.SYSTEM), false)).isTrue();
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildOngoing_summaryOngoing_alwaysAutogroup() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildOngoing_summaryOngoing() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel_alwaysAutogroup() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+            getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+                r.getNotification().flags |= FLAG_AUTO_CANCEL;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+                r.getNotification().flags |= FLAG_AUTO_CANCEL;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(autogroupKey), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel_alwaysAutogroup() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(autogroupKey), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_summaryAutoCancelNoClear_alwaysAutogroup() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_NO_CLEAR;
+                r.getNotification().flags |= FLAG_NO_CLEAR;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_summaryAutoCancelNoClear() {
         final String pkg = "package";
+        final String autogroupKey = getExpectedAutogroupKey(
+                getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM));
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_NO_CLEAR;
+                r.getNotification().flags |= FLAG_NO_CLEAR;
             }
-            mGroupHelper.onNotificationPosted(sbn, false);
+            mGroupHelper.onNotificationPosted(r, false);
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(autogroupKey), anyInt(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(autogroupKey), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
@@ -475,15 +591,16 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_ONGOING_EVENT;
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // One notification is no longer ongoing
@@ -491,7 +608,7 @@
         mGroupHelper.onNotificationPosted(notifications.get(0), true);
 
         // Summary should keep FLAG_ONGOING_EVENT if any child has it
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
     }
 
@@ -500,24 +617,25 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
             }
-            notifications.add(sbn);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // remove ongoing
         mGroupHelper.onNotificationRemoved(notifications.get(0));
 
         // Summary is no longer ongoing
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
     }
 
@@ -526,14 +644,15 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // update to ongoing
@@ -541,7 +660,7 @@
         mGroupHelper.onNotificationPosted(notifications.get(0), true);
 
         // Summary is now ongoing
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
     }
 
@@ -550,23 +669,25 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // add ongoing
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT + 1, null, UserHandle.SYSTEM);
-        sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1, null,
+                UserHandle.SYSTEM);
+        r.getNotification().flags |= FLAG_ONGOING_EVENT;
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Summary is now ongoing
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
     }
 
@@ -575,51 +696,84 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
             }
-            notifications.add(sbn);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // app group the ongoing child
-        StatusBarNotification sbn = getSbn(pkg, 0, "0", UserHandle.SYSTEM, "app group now");
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM,
+                "app group now", false);
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Summary is no longer ongoing
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
     }
 
     @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutoGrouped_singleOngoing_removeNonOngoingChild() {
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i == 0) {
-                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
             }
-            notifications.add(sbn);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // remove ongoing
         mGroupHelper.onNotificationRemoved(notifications.get(1));
 
         // Summary is still ongoing
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutoGrouped_singleOngoing_removeNonOngoingChild_forceGrouping() {
+        final String pkg = "package";
+
+        // Post AUTOGROUP_AT_COUNT ongoing notifications
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            if (i == 0) {
+                r.getNotification().flags |= FLAG_ONGOING_EVENT;
+            }
+            notifications.add(r);
+        }
+
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
+        }
+
+        // remove ongoing
+        mGroupHelper.onNotificationRemoved(notifications.get(1));
+
+        // Summary is still ongoing
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
@@ -627,15 +781,16 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // One notification is no longer autocancelable
@@ -643,7 +798,7 @@
         mGroupHelper.onNotificationPosted(notifications.get(0), true);
 
         // Summary should no longer be autocancelable
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
     }
 
@@ -652,17 +807,18 @@
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
             if (i != 0) {
-                sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+                r.getNotification().flags |= FLAG_AUTO_CANCEL;
             }
-            notifications.add(sbn);
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // Missing notification is now autocancelable
@@ -670,254 +826,327 @@
         mGroupHelper.onNotificationPosted(notifications.get(0), true);
 
         // Summary should now autocancelable
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
     }
 
     @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutoGrouped_allAutoCancel_updateChildAppGrouped() {
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // One notification is now grouped by app
-        StatusBarNotification sbn = getSbn(pkg, 0, "0", UserHandle.SYSTEM, "app group now");
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM,
+                "app group now", false);
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Summary should be still be autocancelable
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutoGrouped_allAutoCancel_updateChildAppGrouped_forceGrouping() {
+        final String pkg = "package";
+
+        // Post AUTOGROUP_AT_COUNT ongoing notifications
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            notifications.add(r);
+        }
+
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
+        }
+
+        // One notification is now grouped by app
+        NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM,
+            "app group now", false);
+        mGroupHelper.onNotificationPosted(r, true);
+
+        // Summary should be still be autocancelable
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
+    }
+
+    @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutoGrouped_allAutoCancel_removeChild() {
         final String pkg = "package";
 
         // Post AUTOGROUP_AT_COUNT ongoing notifications
-        ArrayList<StatusBarNotification>  notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            notifications.add(r);
         }
 
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         mGroupHelper.onNotificationRemoved(notifications.get(0));
 
         // Summary should still be autocancelable
-        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutoGrouped_allAutoCancel_removeChild_forceGrouping() {
+        final String pkg = "package";
+
+        // Post AUTOGROUP_AT_COUNT ongoing notifications
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            r.getNotification().flags |= FLAG_AUTO_CANCEL;
+            notifications.add(r);
+        }
+
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
+        }
+
+        mGroupHelper.onNotificationRemoved(notifications.get(0));
+
+        // Summary should still be autocancelable
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testDropToZeroRemoveGroup_disableFlag() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(),
+                anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
             mGroupHelper.onNotificationRemoved(posted.remove(0));
         }
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         mGroupHelper.onNotificationRemoved(posted.remove(0));
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testDropToZeroRemoveGroup() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(),
+                anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
             mGroupHelper.onNotificationRemoved(posted.remove(0));
         }
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         mGroupHelper.onNotificationRemoved(posted.remove(0));
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAppStartsGrouping_disableFlag() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn =
-                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "app group");
-            sbn.setOverrideGroupKey("autogrouped");
-            mGroupHelper.onNotificationPosted(sbn, true);
-            verify(mCallback, times(1)).removeAutoGroup(sbn.getKey());
+            final NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "app group", false);
+            r.getSbn().setOverrideGroupKey("autogrouped");
+            mGroupHelper.onNotificationPosted(r, true);
+            verify(mCallback, times(1)).removeAutoGroup(r.getKey());
             if (i < AUTOGROUP_AT_COUNT - 1) {
-                verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+                verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(),
+                        anyString());
             }
         }
-        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAppStartsGrouping() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn =
-                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "app group");
-            sbn.setOverrideGroupKey("autogrouped");
-            mGroupHelper.onNotificationPosted(sbn, true);
-            verify(mCallback, times(1)).removeAutoGroup(sbn.getKey());
+            final NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "app group", false);
+            r.getSbn().setOverrideGroupKey("autogrouped");
+            mGroupHelper.onNotificationPosted(r, true);
+            verify(mCallback, times(1)).removeAutoGroup(r.getKey());
             if (i < AUTOGROUP_AT_COUNT - 1) {
-                verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+                verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(),
+                        anyString());
             }
         }
-        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString());
     }
 
     @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = posted.size() - 2; i >= 0; i--) {
             mGroupHelper.onNotificationRemoved(posted.remove(i));
         }
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
-        // only one child remains
-        assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg));
-
         // Add new notification; it should be autogrouped even though the total count is
         // < AUTOGROUP_AT_COUNT
-        final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
-        posted.add(sbn);
-        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isFalse();
-        verify(mCallback, times(1)).addAutoGroup(sbn.getKey(), true);
+        final NotificationRecord r = getNotificationRecord(pkg, 5, String.valueOf(5),
+                UserHandle.SYSTEM);
+        final String autogroupKey = getExpectedAutogroupKey(r);
+        posted.add(r);
+        assertThat(mGroupHelper.onNotificationPosted(r, true)).isFalse();
+        verify(mCallback, times(1)).addAutoGroup(r.getKey(), autogroupKey, true);
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any());
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
     }
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() {
         final String pkg = "package";
-        List<StatusBarNotification> posted = new ArrayList<>();
+        ArrayList<NotificationRecord> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
-            posted.add(sbn);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM);
+            posted.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
         for (int i = posted.size() - 2; i >= 0; i--) {
             mGroupHelper.onNotificationRemoved(posted.remove(i));
         }
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
         Mockito.reset(mCallback);
 
-        // only one child remains
-        assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg));
-
         // Add new notification; it should be autogrouped even though the total count is
         // < AUTOGROUP_AT_COUNT
-        final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
-        posted.add(sbn);
-        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+        final NotificationRecord r = getNotificationRecord(pkg, 5, String.valueOf(5),
+                UserHandle.SYSTEM);
+        posted.add(r);
+        assertThat(mGroupHelper.onNotificationPosted(r, true)).isTrue();
         // addAutoGroup not called on sbn, because the autogrouping is expected to be done
         // synchronously.
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
-        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any());
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
     }
 
     @Test
@@ -929,29 +1158,32 @@
         when(icon.sameAs(icon)).thenReturn(true);
         final int iconColor = Color.BLUE;
         final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                DEFAULT_VISIBILITY);
+                DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    icon, iconColor);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(
+                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, icon, iconColor));
+            mGroupHelper.onNotificationPosted(r, false);
         }
         // Check that the summary would have the same icon and color
         verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(attr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+                anyInt(), eq(pkg), anyString(), anyString(), anyInt(), eq(attr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with the same color
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
-                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, AUTOGROUP_AT_COUNT, String.valueOf(AUTOGROUP_AT_COUNT),
+                    UserHandle.SYSTEM,null, icon, iconColor));
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Check that the summary was updated
         //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(attr));
     }
 
     @Test
@@ -963,29 +1195,31 @@
         when(icon.sameAs(icon)).thenReturn(true);
         final int iconColor = Color.BLUE;
         final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                DEFAULT_VISIBILITY);
+                DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    icon, iconColor);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(
+                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, icon, iconColor));
+            mGroupHelper.onNotificationPosted(r, false);
         }
         // Check that the summary would have the same icon and color
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(attr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(attr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with the same color
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
-                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor));
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Check that the summary was updated
         //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(attr));
     }
 
     @Test
@@ -1004,33 +1238,37 @@
         doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
 
         final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS,
-                initialIcon, initialIconColor, DEFAULT_VISIBILITY);
+                initialIcon, initialIconColor, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    initialIcon, initialIconColor);
-            groupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    initialIcon, initialIconColor));
+            groupHelper.onNotificationPosted(r, false);
         }
         // Check that the summary would have the same icon and color
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(initialAttr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(initialAttr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with a different color
         final Icon newIcon = mock(Icon.class);
         final int newIconColor = Color.YELLOW;
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+        NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT,
                 String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon,
-                newIconColor);
-        groupHelper.onNotificationPosted(sbn, true);
+                newIconColor));
+        groupHelper.onNotificationPosted(r, true);
 
         // Summary should be updated to the default color and the icon to the monochrome icon
         NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon,
-                COLOR_DEFAULT, DEFAULT_VISIBILITY);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+                COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(newAttr));
     }
 
     @Test
@@ -1049,33 +1287,37 @@
         doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
 
         final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS,
-                initialIcon, initialIconColor, DEFAULT_VISIBILITY);
+                initialIcon, initialIconColor, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    initialIcon, initialIconColor);
-            groupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    initialIcon, initialIconColor));
+            groupHelper.onNotificationPosted(r, false);
         }
         // Check that the summary would have the same icon and color
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(initialAttr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), eq(initialAttr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with a different color
         final Icon newIcon = mock(Icon.class);
         final int newIconColor = Color.YELLOW;
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
-                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon,
-                newIconColor);
-        groupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon,
+            newIconColor));
+        groupHelper.onNotificationPosted(r, true);
 
         // Summary should be updated to the default color and the icon to the monochrome icon
         NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon,
-                COLOR_DEFAULT, DEFAULT_VISIBILITY);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+                COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(newAttr));
     }
 
     @Test
@@ -1087,32 +1329,35 @@
         when(icon.sameAs(icon)).thenReturn(true);
         final int iconColor = Color.BLUE;
         final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                VISIBILITY_PRIVATE);
+                VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color and default visibility (private)
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    icon, iconColor);
-            mGroupHelper.onNotificationPosted(sbn, false);
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    icon, iconColor));
+            mGroupHelper.onNotificationPosted(r, false);
         }
         // Check that the summary has private visibility
         verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(attr));
+                anyInt(), eq(pkg), anyString(), anyString(), anyInt(), eq(attr));
 
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with public visibility
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
-                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
-        sbn.getNotification().visibility = VISIBILITY_PUBLIC;
-        mGroupHelper.onNotificationPosted(sbn, true);
+        NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor));
+        r.getNotification().visibility = VISIBILITY_PUBLIC;
+        mGroupHelper.onNotificationPosted(r, true);
 
         // Check that the summary visibility was updated
         NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                VISIBILITY_PUBLIC);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+                VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(newAttr));
     }
 
     @Test
@@ -1124,71 +1369,116 @@
         when(icon.sameAs(icon)).thenReturn(true);
         final int iconColor = Color.BLUE;
         final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                VISIBILITY_PRIVATE);
+                VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
 
         // Add notifications with same icon and color and default visibility (private)
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    icon, iconColor);
-            assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isFalse();
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    icon, iconColor));
+            assertThat(mGroupHelper.onNotificationPosted(r, false)).isFalse();
         }
         // The last notification added will reach the autogroup threshold.
-        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT - 1,
-                String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor);
-        assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isTrue();
+        NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT - 1,
+            String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor));
+        assertThat(mGroupHelper.onNotificationPosted(r, false)).isTrue();
 
         // Check that the summary has private visibility
-        verify(mCallback, times(1)).addAutoGroupSummary(
-                anyInt(), eq(pkg), anyString(), eq(attr));
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(),
+                anyInt(), eq(attr));
         // The last sbn is expected to be added to autogroup synchronously.
-        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(),
+                anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
-        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
 
         // After auto-grouping, add new notification with public visibility
-        sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
-                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
-        sbn.getNotification().visibility = VISIBILITY_PUBLIC;
-        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+        r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor));
+        r.getNotification().visibility = VISIBILITY_PUBLIC;
+        assertThat(mGroupHelper.onNotificationPosted(r, true)).isTrue();
 
         // Check that the summary visibility was updated
         NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
-                VISIBILITY_PUBLIC);
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+                VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(newAttr));
     }
 
     @Test
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor() {
         final String pkg = "package";
         final Icon initialIcon = mock(Icon.class);
         when(initialIcon.sameAs(initialIcon)).thenReturn(true);
         final int initialIconColor = Color.BLUE;
         final NotificationAttributes initialAttr = new NotificationAttributes(
-                GroupHelper.FLAG_INVALID, initialIcon, initialIconColor, DEFAULT_VISIBILITY);
+                GroupHelper.FLAG_INVALID, initialIcon, initialIconColor, DEFAULT_VISIBILITY,
+                DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
 
         // Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color
-        ArrayList<StatusBarNotification> notifications = new ArrayList<>();
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
-            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
-                    initialIcon, initialIconColor);
-            notifications.add(sbn);
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    initialIcon, initialIconColor));
+            notifications.add(r);
         }
         // And an additional notification with different icon and color
         final int lastIdx = AUTOGROUP_AT_COUNT - 1;
-        StatusBarNotification newSbn = getSbn(pkg, lastIdx,
+        NotificationRecord newRec = getNotificationRecord(getSbn(pkg, lastIdx,
                 String.valueOf(lastIdx), UserHandle.SYSTEM, null, mock(Icon.class),
-                Color.YELLOW);
-        notifications.add(newSbn);
-        for (StatusBarNotification sbn: notifications) {
-            mGroupHelper.onNotificationPosted(sbn, false);
+                Color.YELLOW));
+        notifications.add(newRec);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
         }
 
         // Remove last notification (the only one with different icon and color)
         mGroupHelper.onNotificationRemoved(notifications.get(lastIdx));
 
         // Summary should be updated to the common icon and color
-        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(initialAttr));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                eq(initialAttr));
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+            FLAG_NOTIFICATION_FORCE_GROUPING})
+    public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor_forceGrouping() {
+        final String pkg = "package";
+        final Icon initialIcon = mock(Icon.class);
+        when(initialIcon.sameAs(initialIcon)).thenReturn(true);
+        final int initialIconColor = Color.BLUE;
+        final NotificationAttributes initialAttr = new NotificationAttributes(
+            BASE_FLAGS, initialIcon, initialIconColor, DEFAULT_VISIBILITY,
+            DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID);
+
+        // Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color
+        ArrayList<NotificationRecord> notifications = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(
+                getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    initialIcon, initialIconColor));
+            notifications.add(r);
+        }
+        // And an additional notification with different icon and color
+        final int lastIdx = AUTOGROUP_AT_COUNT - 1;
+        NotificationRecord newRec = getNotificationRecord(getSbn(pkg, lastIdx,
+            String.valueOf(lastIdx), UserHandle.SYSTEM, null, mock(Icon.class),
+            Color.YELLOW));
+        notifications.add(newRec);
+        for (NotificationRecord r: notifications) {
+            mGroupHelper.onNotificationPosted(r, false);
+        }
+
+        // Remove last notification (the only one with different icon and color)
+        mGroupHelper.onNotificationRemoved(notifications.get(lastIdx));
+
+        // Summary should be updated to the common icon and color
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            eq(initialAttr));
     }
 
     @Test
@@ -1202,7 +1492,7 @@
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             childrenAttr.add(new NotificationAttributes(0, icon, COLOR_DEFAULT,
-                    DEFAULT_VISIBILITY));
+                    DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         //Check that the generated summary icon is the same as the child notifications'
@@ -1223,7 +1513,7 @@
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), COLOR_DEFAULT,
-                    DEFAULT_VISIBILITY));
+                    DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         // Check that the generated summary icon is the monochrome icon
@@ -1240,7 +1530,7 @@
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
-                    DEFAULT_VISIBILITY));
+                    DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         // Check that the generated summary icon color is the same as the child notifications'
@@ -1257,7 +1547,7 @@
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), i,
-                    DEFAULT_VISIBILITY));
+                    DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         // Check that the generated summary icon color is the default color
@@ -1274,10 +1564,10 @@
         // Create notifications with private and public visibility
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
         childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
-                VISIBILITY_PUBLIC));
+                VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
             childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
-                    VISIBILITY_PRIVATE));
+                    VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         // Check that the generated summary visibility is public
@@ -1301,7 +1591,7 @@
                 visibility = VISIBILITY_SECRET;
             }
             childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
-                    visibility));
+                    visibility, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID));
         }
 
         // Check that the generated summary visibility is private
@@ -1311,6 +1601,90 @@
     }
 
     @Test
+    public void testAutobundledSummaryAlertBehavior_oneChildAlertChildren() {
+        final String pkg = "package";
+        final int iconColor = Color.BLUE;
+        // Create notifications with GROUP_ALERT_SUMMARY + one with GROUP_ALERT_CHILDREN
+        List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                VISIBILITY_PUBLIC, GROUP_ALERT_CHILDREN, TEST_CHANNEL_ID));
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID));
+        }
+        // Check that the generated summary alert behavior is GROUP_ALERT_CHILDREN
+        int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+                childrenAttr).groupAlertBehavior;
+        assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_CHILDREN);
+    }
+
+    @Test
+    public void testAutobundledSummaryAlertBehavior_oneChildAlertAll() {
+        final String pkg = "package";
+        final int iconColor = Color.BLUE;
+        // Create notifications with GROUP_ALERT_SUMMARY + one with GROUP_ALERT_ALL
+        List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                VISIBILITY_PUBLIC, GROUP_ALERT_ALL, TEST_CHANNEL_ID));
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID));
+        }
+        // Check that the generated summary alert behavior is GROUP_ALERT_CHILDREN
+        int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+                childrenAttr).groupAlertBehavior;
+        assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_CHILDREN);
+    }
+
+    @Test
+    public void testAutobundledSummaryAlertBehavior_allChildAlertSummary() {
+        final String pkg = "package";
+        final int iconColor = Color.BLUE;
+        // Create notifications with GROUP_ALERT_SUMMARY
+        List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID));
+        }
+
+        // Check that the generated summary alert behavior is GROUP_ALERT_SUMMARY
+        int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+                childrenAttr).groupAlertBehavior;
+        assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_SUMMARY);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+    public void testAutobundledSummaryChannelId() {
+        final String pkg = "package";
+        final int iconColor = Color.BLUE;
+        final String expectedChannelId = TEST_CHANNEL_ID + "0";
+        // Create notifications with different channelIds
+        List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+                    DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID+i));
+        }
+
+        // Check that the generated summary channelId is the first child in the list
+        String summaryChannelId = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+                childrenAttr).channelId;
+        assertThat(summaryChannelId).isEqualTo(expectedChannelId);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+    public void testAutobundledSummaryChannelId_noChildren() {
+        final String pkg = "package";
+        // No child notifications
+        List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        // Check that the generated summary channelId is null
+        String summaryChannelId = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+                childrenAttr).channelId;
+        assertThat(summaryChannelId).isNull();
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
     public void testMonochromeAppIcon_adaptiveIconExists() throws Exception {
         final String pkg = "testPackage";
@@ -1333,4 +1707,855 @@
         assertThat(mGroupHelper.getMonochromeAppIcon(pkg).getResId())
                 .isEqualTo(fallbackIconResId);
     }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testGetAggregateGroupKey() {
+        final String fullAggregateGroupKey = GroupHelper.getFullAggregateGroupKey("pkg",
+                "groupKey", 1234);
+        assertThat(fullAggregateGroupKey).isEqualTo("1234|pkg|g:groupKey");
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_postingUnderLimit_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_AutobundledAlready_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, null, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_isCanceled_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp" + i, true);
+            r.isCanceled = true;
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_isAggregated_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            String aggregateGroupKey = AGGREGATE_GROUP_KEY + "AlertingSection";
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, aggregateGroupKey, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_multiPackage_forcedGrouping() {
+        final String pkg = "package";
+        final String pkg2 = "package2";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        NotificationRecord r = getNotificationRecord(pkg2, AUTOGROUP_AT_COUNT,
+                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(r);
+        mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_multiUser_forcedGrouping() {
+        final String pkg = "package";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        NotificationRecord r = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT,
+                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.of(7), "testGrp", true);
+        notificationList.add(r);
+        mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_summaryWithChildren_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        // Next posted summary has 1 child => no forced grouping
+        NotificationRecord summary = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(summary);
+        NotificationRecord child = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1,
+            String.valueOf(AUTOGROUP_AT_COUNT + 1), UserHandle.SYSTEM, "testGrp", false);
+        notificationList.add(child);
+        mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testNoGroup_groupWithSummary_forcedGrouping() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        // Next posted notification has summary => no forced grouping
+        NotificationRecord summary = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT,
+            String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(summary);
+        NotificationRecord child = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1,
+            String.valueOf(AUTOGROUP_AT_COUNT + 1), UserHandle.SYSTEM, "testGrp", false);
+        notificationList.add(child);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAddAggregateSummary_summaryNoChildren() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group summaries without children => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, true);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+            eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAddAggregateSummary_childrenNoSummary() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, false);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAddAggregateSummary_multipleSections() {
+        final String pkg = "package";
+        final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post notifications with different importance values => force group into separate sections
+        NotificationRecord r;
+        for (int i = 0; i < 2 * AUTOGROUP_AT_COUNT; i++) {
+            if (i % 2 == 0) {
+                r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM,
+                    "testGrp " + i, true, IMPORTANCE_DEFAULT);
+            } else {
+                r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM,
+                    "testGrp " + i, false, IMPORTANCE_LOW);
+            }
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_alerting), eq(true));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+            eq(expectedGroupKey_silent), eq(true));
+
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+    }
+
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddAggregateSummary_mixUngroupedAndAbusive_alwaysAutogroup() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        // Post ungrouped notifications => create autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            mGroupHelper.onNotificationPosted(
+                getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(expectedGroupKey),
+                anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(expectedGroupKey),
+                anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+
+        reset(mCallback);
+
+        // Post group notifications without summaries => add to autogroup
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final int id = AUTOGROUP_AT_COUNT;
+        NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id),
+                UserHandle.SYSTEM, "testGrp " + id, false);
+        notificationList.add(r);
+        mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+
+        // Check that the new notification was added
+        verify(mCallback, times(1)).addAutoGroup(eq(r.getKey()),
+                eq(expectedGroupKey), eq(true));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey), any());
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testUpdateAggregateSummary_postUngroupedAfterForcedGrouping_alwaysAutogroup() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, false);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+            eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
+
+        reset(mCallback);
+
+        // Post ungrouped notification => update autogroup
+        final int id = AUTOGROUP_AT_COUNT;
+        NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id),
+                UserHandle.SYSTEM);
+        mGroupHelper.onNotificationPosted(r, true);
+
+        verify(mCallback, times(1)).addAutoGroup(eq(r.getKey()),
+                eq(expectedGroupKey), eq(true));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testUpdateAggregateSummary_postUngroupedAfterForcedGrouping() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, false);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+            eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
+
+        reset(mCallback);
+
+        // Post ungrouped notification => update autogroup
+        final int id = AUTOGROUP_AT_COUNT;
+        NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id),
+            UserHandle.SYSTEM);
+        mGroupHelper.onNotificationPosted(r, true);
+
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg),
+            eq(expectedGroupKey), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+            anyString(), anyInt(), any());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testUpdateAggregateSummary_postAfterForcedGrouping() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications w/o summaries and summaries w/o children => force autogrouping
+        NotificationRecord r;
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            if (i % 2 == 0) {
+                r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM,
+                    "testGrp " + i, true);
+            } else {
+                r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM,
+                    "testGrp " + i, false);
+            }
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+
+        // Post another notification after forced grouping
+        final Icon icon = mock(Icon.class);
+        when(icon.sameAs(icon)).thenReturn(true);
+        final int iconColor = Color.BLUE;
+        r = getNotificationRecord(
+                getSbn(pkg, AUTOGROUP_AT_COUNT, String.valueOf(AUTOGROUP_AT_COUNT),
+                    UserHandle.SYSTEM, "testGrp " + AUTOGROUP_AT_COUNT, icon, iconColor));
+
+        notificationList.add(r);
+        mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+            eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT + 1)).addAutoGroup(anyString(),
+            eq(expectedGroupKey), eq(true));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg),
+            eq(expectedGroupKey), any());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testRemoveAggregateSummary_removeAllNotifications() {
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                UserHandle.SYSTEM, "testGrp " + i, false);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        Mockito.reset(mCallback);
+
+        // Remove all posted notifications
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, false);
+            r.setOverrideGroupKey(expectedGroupKey);
+            mGroupHelper.onNotificationRemoved(r, notificationList);
+        }
+        // Check that the autogroup summary is removed
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testMoveAggregateGroups_updateChannel() {
+        final String pkg = "package";
+        final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final NotificationChannel channel = new NotificationChannel(TEST_CHANNEL_ID,
+                TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, false, channel);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_alerting), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+            any());
+        Mockito.reset(mCallback);
+
+        // Update the channel importance for all posted notifications
+        final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        channel.setImportance(IMPORTANCE_LOW);
+        for (NotificationRecord r: notificationList) {
+            r.updateNotificationChannel(channel);
+        }
+        mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel,
+                notificationList);
+
+        // Check that all notifications are moved to the silent section group
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_silent), eq(false));
+
+        // Check that the alerting section group is removed
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_alerting));
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testMoveAggregateGroups_updateChannel_multipleChannels() {
+        final String pkg = "package";
+        final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1",
+            "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT);
+        final NotificationChannel channel2 = new NotificationChannel("TEST_CHANNEL_ID2",
+            "TEST_CHANNEL_ID2", IMPORTANCE_DEFAULT);
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post notifications with different channels that autogroup within the same section
+        NotificationRecord r;
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            if (i % 2 == 0) {
+                r = getNotificationRecord(pkg, i, String.valueOf(i),
+                        UserHandle.SYSTEM, "testGrp " + i, false, channel1);
+            } else {
+                r = getNotificationRecord(pkg, i, String.valueOf(i),
+                        UserHandle.SYSTEM, "testGrp " + i, false, channel2);
+            }
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
+                mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                "TEST_CHANNEL_ID1");
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_alerting), anyInt(), eq(expectedSummaryAttr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_alerting), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        Mockito.reset(mCallback);
+
+        // Update channel1's importance
+        final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        channel1.setImportance(IMPORTANCE_LOW);
+        for (NotificationRecord record: notificationList) {
+            if (record.getChannel().getId().equals(channel1.getId())) {
+                record.updateNotificationChannel(channel1);
+            }
+        }
+        mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1,
+                notificationList);
+
+        // Check that channel1's notifications are moved to the silent section group
+        expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
+                mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                "TEST_CHANNEL_ID1");
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT/2 + 1)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_silent), eq(false));
+
+        // Check that the alerting section group is not removed, only updated
+        expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
+            mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+            "TEST_CHANNEL_ID2");
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_alerting));
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_alerting), eq(expectedSummaryAttr));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testMoveAggregateGroups_updateChannel_groupsUngrouped() {
+        final String pkg = "package";
+        final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+
+        // Post too few group notifications without summaries => do not autogroup
+        final NotificationChannel lowPrioChannel = new NotificationChannel("TEST_CHANNEL_LOW_ID",
+                "TEST_CHANNEL_LOW_ID", IMPORTANCE_LOW);
+        final int numUngrouped = AUTOGROUP_AT_COUNT - 1;
+        int startIdx = 42;
+        for (int i = startIdx; i < startIdx + numUngrouped; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, false, lowPrioChannel);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+
+        reset(mCallback);
+
+        final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final NotificationChannel channel = new NotificationChannel(TEST_CHANNEL_ID,
+                TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+
+        // Post group notifications without summaries => force autogroup
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, "testGrp " + i, false, channel);
+            notificationList.add(r);
+            mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_alerting), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        Mockito.reset(mCallback);
+
+        // Update the channel importance for all posted notifications
+        final int numSilentGroupNotifications = AUTOGROUP_AT_COUNT + numUngrouped;
+        channel.setImportance(IMPORTANCE_LOW);
+        for (NotificationRecord r: notificationList) {
+            r.updateNotificationChannel(channel);
+        }
+        mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel,
+                notificationList);
+
+        // Check that all notifications are moved to the silent section group
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(numSilentGroupNotifications)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_silent), eq(false));
+
+        // Check that the alerting section group is removed
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+                eq(expectedGroupKey_alerting));
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testNoGroup_singletonGroup_underLimit() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        // Post singleton groups, under forced group limit
+        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT - 1; i++) {
+            NotificationRecord summary = getNotificationRecord(pkg, i,
+                    String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true);
+            notificationList.add(summary);
+            NotificationRecord child = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false);
+            notificationList.add(child);
+            summaryByGroup.put(summary.getGroupKey(), summary);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+            mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+        }
+        verifyZeroInteractions(mCallback);
+    }
+
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @DisableFlags(Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS)
+    public void testAddAggregateSummary_singletonGroup_disableFlag() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        // Post singleton groups, above forced group limit
+        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+            NotificationRecord summary = getNotificationRecord(pkg, i,
+                    String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true);
+            notificationList.add(summary);
+            NotificationRecord child = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false);
+            notificationList.add(child);
+            summaryByGroup.put(summary.getGroupKey(), summary);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+            mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+        }
+        // FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS is disabled => don't force group
+        verifyZeroInteractions(mCallback);
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testAddAggregateSummary_singletonGroups() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        // Post singleton groups, above forced group limit
+        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+            NotificationRecord summary = getNotificationRecord(pkg, i,
+                String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true);
+            notificationList.add(summary);
+            NotificationRecord child = getNotificationRecord(pkg, i + 42,
+                String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false);
+            notificationList.add(child);
+            summaryByGroup.put(summary.getGroupKey(), summary);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+            summary.isCanceled = true;  // simulate removing the app summary
+            mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+
+        }
+        // Check that notifications are forced grouped
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+
+        // Check that summaries are canceled
+        verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).removeAppProvidedSummary(anyString());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testCancelCachedSummary_singletonGroups() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        final int id = 0;
+        // Post singleton groups, above forced group limit
+        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+            NotificationRecord summary = getNotificationRecord(pkg, i,
+                    String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true);
+            notificationList.add(summary);
+            NotificationRecord child = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false);
+            notificationList.add(child);
+            summaryByGroup.put(summary.getGroupKey(), summary);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+            summary.isCanceled = true;  // simulate removing the app summary
+            mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+        }
+        Mockito.reset(mCallback);
+
+        // App cancels the summary of an aggregated group
+        mGroupHelper.maybeCancelGroupChildrenForCanceledSummary(pkg, String.valueOf(id), id,
+                UserHandle.SYSTEM.getIdentifier(), REASON_APP_CANCEL);
+
+        verify(mCallback, times(1)).removeNotificationFromCanceledGroup(
+                eq(UserHandle.SYSTEM.getIdentifier()), eq(pkg), eq("testGrp " + id),
+                eq(REASON_APP_CANCEL));
+        CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id,
+                UserHandle.SYSTEM.getIdentifier());
+        assertThat(cachedSummary).isNull();
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testRemoveCachedSummary_singletonGroups_removeChildren() {
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+            AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        final int id = 0;
+        NotificationRecord childToRemove = null;
+        // Post singleton groups, above forced group limit
+        for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) {
+            NotificationRecord summary = getNotificationRecord(pkg, i,
+                    String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true);
+            notificationList.add(summary);
+            NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+                    UserHandle.SYSTEM, "testGrp " + i, false);
+            if (i == id) {
+                childToRemove = child;
+            }
+            notificationList.add(child);
+            summaryByGroup.put(summary.getGroupKey(), summary);
+            mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+            summary.isCanceled = true;  // simulate removing the app summary
+            mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup);
+        }
+        // override group key for child notifications
+        List<NotificationRecord> notificationListAfterGrouping = new ArrayList<>(
+            notificationList.stream().filter(r -> {
+                if (r.getSbn().getNotification().isGroupChild()) {
+                    r.setOverrideGroupKey(expectedGroupKey);
+                    return true;
+                } else {
+                    return false;
+                }
+            }).toList());
+        summaryByGroup.clear();
+        Mockito.reset(mCallback);
+
+        //Cancel child 0 => remove cached summary
+        childToRemove.isCanceled = true;
+        notificationListAfterGrouping.remove(childToRemove);
+        mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping);
+        CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id,
+                UserHandle.SYSTEM.getIdentifier());
+        assertThat(cachedSummary).isNull();
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testGroupSectioners() {
+        final NotificationRecord notification_alerting = getNotificationRecord(mPkg, 0, "", mUser,
+            "", false, IMPORTANCE_DEFAULT);
+        assertThat(GroupHelper.getSection(notification_alerting).mName).isEqualTo("AlertingSection");
+
+        final NotificationRecord notification_silent = getNotificationRecord(mPkg, 0, "", mUser,
+            "", false, IMPORTANCE_LOW);
+        assertThat(GroupHelper.getSection(notification_silent).mName).isEqualTo("SilentSection");
+
+        NotificationRecord notification_conversation = mock(NotificationRecord.class);
+        when(notification_conversation.isConversation()).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_conversation)).isNull();
+
+        NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser,
+                "", false, IMPORTANCE_LOW));
+        Notification n = mock(Notification.class);
+        StatusBarNotification sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+        when(notification_call.isConversation()).thenReturn(false);
+        when(notification_call.getNotification()).thenReturn(n);
+        when(notification_call.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(n);
+        when(n.isStyle(Notification.CallStyle.class)).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_call)).isNull();
+
+        NotificationRecord notification_colorFg = spy(getNotificationRecord(mPkg, 0, "", mUser,
+            "", false, IMPORTANCE_LOW));
+        sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+        n = mock(Notification.class);
+        when(notification_colorFg.isConversation()).thenReturn(false);
+        when(notification_colorFg.getNotification()).thenReturn(n);
+        when(notification_colorFg.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(n);
+        when(n.isForegroundService()).thenReturn(true);
+        when(n.isColorized()).thenReturn(true);
+        when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+        assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+    }
+
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c48d745..5d306e1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -39,8 +39,10 @@
 import static android.app.Notification.FLAG_ONGOING_EVENT;
 import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
 import static android.app.Notification.FLAG_USER_INITIATED_JOB;
+import static android.app.Notification.GROUP_ALERT_CHILDREN;
 import static android.app.Notification.VISIBILITY_PRIVATE;
 import static android.app.NotificationChannel.NEWS_ID;
+import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
 import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE;
 import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
@@ -98,11 +100,13 @@
 import static android.service.notification.Condition.SOURCE_CONTEXT;
 import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
 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;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
@@ -117,6 +121,7 @@
 import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
 import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
 import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS;
+import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY;
 import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION;
 import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
 import static com.android.server.notification.NotificationManagerService.NOTIFICATION_TTL;
@@ -369,6 +374,7 @@
     private static final int TOAST_DURATION = 2_000;
     private static final int SECONDARY_DISPLAY_ID = 42;
     private static final int TEST_PROFILE_USERHANDLE = 12;
+    private static final long DELAY_FORCE_REGROUP_TIME = 3000;
 
     private static final String ACTION_NOTIFICATION_TIMEOUT =
             NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
@@ -2487,43 +2493,372 @@
     }
 
     @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutobundledSummary_notificationAdded() {
         NotificationRecord summary =
-                generateNotificationRecord(mTestNotificationChannel, 0, "pkg", true);
+                generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true);
         summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY;
         mService.addNotification(summary);
         mService.mSummaryByGroupKey.put("pkg", summary);
         mService.mAutobundledSummaries.put(0, new ArrayMap<>());
         mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey());
 
-        mService.updateAutobundledSummaryLocked(0, "pkg",
+        mService.updateAutobundledSummaryLocked(0, "pkg", AUTOGROUP_KEY,
                 new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT,
-                    mock(Icon.class), 0, VISIBILITY_PRIVATE), false);
+                    mock(Icon.class), 0,
+                    VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false);
         waitForIdle();
 
         assertTrue(summary.getSbn().isOngoing());
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutobundledSummary_notificationAdded_forcedGrouping() {
+        NotificationRecord summary =
+                generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true);
+        summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY;
+        mService.addNotification(summary);
+        mService.mSummaryByGroupKey.put("pkg", summary);
+        mService.mAutobundledSummaries.put(0, new ArrayMap<>());
+        mService.mAutobundledSummaries.get(0).put(summary.getGroupKey(), summary.getKey());
+
+        mService.updateAutobundledSummaryLocked(0, "pkg", summary.getGroupKey(),
+                new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT,
+                    mock(Icon.class), 0,
+                    VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false);
+        waitForIdle();
+
+        assertTrue(summary.getSbn().isOngoing());
+    }
+
+    @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testAutobundledSummary_notificationRemoved() {
         NotificationRecord summary =
-                generateNotificationRecord(mTestNotificationChannel, 0, "pkg", true);
+                generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true);
         summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY;
         summary.getNotification().flags |= Notification.FLAG_ONGOING_EVENT;
         mService.addNotification(summary);
         mService.mAutobundledSummaries.put(0, new ArrayMap<>());
         mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey());
-        mService.mSummaryByGroupKey.put("pkg", summary);
+        mService.mSummaryByGroupKey.put(summary.getGroupKey(), summary);
 
-        mService.updateAutobundledSummaryLocked(0, "pkg",
+        mService.updateAutobundledSummaryLocked(0, "pkg", AUTOGROUP_KEY,
                 new NotificationAttributes(GroupHelper.BASE_FLAGS,
-                    mock(Icon.class), 0, VISIBILITY_PRIVATE), false);
+                    mock(Icon.class), 0,
+                    VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false);
         waitForIdle();
 
         assertFalse(summary.getSbn().isOngoing());
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutobundledSummary_notificationRemoved_forceGrouping() {
+        NotificationRecord summary =
+            generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true);
+        summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY;
+        summary.getNotification().flags |= Notification.FLAG_ONGOING_EVENT;
+        mService.addNotification(summary);
+        mService.mAutobundledSummaries.put(0, new ArrayMap<>());
+        mService.mAutobundledSummaries.get(0).put(summary.getGroupKey(), summary.getKey());
+
+        mService.updateAutobundledSummaryLocked(0, "pkg", summary.getGroupKey(),
+                new NotificationAttributes(GroupHelper.BASE_FLAGS,
+                    mock(Icon.class), 0,
+                    VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false);
+        waitForIdle();
+
+        assertFalse(summary.getSbn().isOngoing());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAggregatedSummary_updateSummaryAttributes() {
+        final String aggregateGroupName = "Aggregate_Test";
+        final String newChannelId = "newChannelId";
+        final NotificationChannel newChannel = new NotificationChannel(
+                newChannelId, newChannelId, IMPORTANCE_DEFAULT);
+        mService.setPreferencesHelper(mPreferencesHelper);
+        final NotificationRecord summary =
+                generateNotificationRecord(mTestNotificationChannel, 0, aggregateGroupName, true);
+        final String groupKey = summary.getGroupKey();
+        summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY;
+        mService.addNotification(summary);
+        mService.mAutobundledSummaries.put(0, new ArrayMap<>());
+        mService.mAutobundledSummaries.get(0).put(groupKey, summary.getKey());
+        when(mPreferencesHelper.getNotificationChannel(eq("pkg"), anyInt(),
+                eq(newChannelId), anyBoolean())).thenReturn(newChannel);
+
+        mService.updateAutobundledSummaryLocked(0, "pkg", groupKey,
+                new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT,
+                    mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, newChannelId),
+                    false);
+        waitForIdle();
+
+        assertTrue(summary.getSbn().isOngoing());
+        assertThat(summary.getNotification().getGroupAlertBehavior()).isEqualTo(
+                GROUP_ALERT_CHILDREN);
+
+        assertThat(summary.getChannel().getId()).isEqualTo(newChannelId);
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAddAggregateNotification_notifyPostedLocked() throws Exception {
+        final String originalGroupName = "originalGroup";
+        final NotificationRecord r =
+                generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+        mService.addNotification(r);
+        mService.addAutogroupKeyLocked(r.getKey(), "grpKey", true);
+
+        assertThat(r.getSbn().getOverrideGroupKey()).isEqualTo("grpKey");
+        verify(mRankingHandler, times(1)).requestSort();
+        verify(mListeners, times(1)).notifyPostedLocked(eq(r), eq(r));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAddAggregateSummaryNotification_convertSummary() throws Exception {
+        final String originalGroupName = "originalGroup";
+        final NotificationRecord r =
+                generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+        final String groupKey = r.getGroupKey();
+        mService.addNotification(r);
+        assertThat(mService.mSummaryByGroupKey.containsKey(groupKey)).isTrue();
+        boolean isConverted = mService.convertSummaryToNotificationLocked(r.getKey());
+
+        assertThat(isConverted).isTrue();
+        assertThat(r.getSbn().isGroup()).isTrue();
+        assertThat(r.getNotification().isGroupSummary()).isFalse();
+        assertThat(mService.mSummaryByGroupKey.containsKey(groupKey)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testAggregateGroups_RemoveAppSummary() throws Exception {
+        final String originalGroupName = "originalGroup";
+        final NotificationRecord r =
+                generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+        mService.addNotification(r);
+        mService.removeAppSummaryLocked(r.getKey());
+
+        assertThat(r.isCanceled).isTrue();
+        waitForIdle();
+        verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(0));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testUngroupingAggregateSummary() throws Exception {
+        final String originalGroupName = "originalGroup";
+        final String aggregateGroupName = "Aggregate_Test";
+        final int summaryId = Integer.MAX_VALUE;
+        // Add 2 group notifications without a summary
+        NotificationRecord nr0 =
+                generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+        NotificationRecord nr1 =
+                generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false);
+        mService.addNotification(nr0);
+        mService.addNotification(nr1);
+        mService.mSummaryByGroupKey.remove(nr0.getGroupKey());
+
+        // GroupHelper is a mock, so make the calls it would make
+        // Add aggregate group summary
+        NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS,
+                mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN,
+                nr0.getChannel().getId());
+        NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(),
+                nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr);
+        mService.addNotification(aggregateSummary);
+        nr0.setOverrideGroupKey(aggregateGroupName);
+        nr1.setOverrideGroupKey(aggregateGroupName);
+        final String fullAggregateGroupKey = nr0.getGroupKey();
+
+        // Check that the aggregate group summary was created
+        assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName);
+        assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo(
+                nr0.getChannel().getId());
+        assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue();
+
+        // Cancel both children
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(),
+                nr0.getSbn().getId(), nr0.getSbn().getUserId());
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1.getSbn().getTag(),
+                nr1.getSbn().getId(), nr1.getSbn().getUserId());
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any());
+        verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any());
+
+        // GroupHelper would send 'remove summary' event
+        mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(),
+                fullAggregateGroupKey);
+        waitForIdle();
+
+        // Make sure the summary was removed and not re-posted
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(0);
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+    public void testCancelGroupChildrenForCanceledSummary_singletonGroup() throws Exception {
+        final String originalGroupName = "originalGroup";
+        final String aggregateGroupName = "Aggregate_Test";
+        final int summaryId = Integer.MAX_VALUE;
+        // Add a "singleton group"
+        NotificationRecord nr0 =
+                generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+        NotificationRecord nr1 =
+                generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false);
+        final NotificationRecord summary =
+                generateNotificationRecord(mTestNotificationChannel, 2, originalGroupName, true);
+        final String originalGroupKey = summary.getGroupKey();
+        mService.addNotification(nr0);
+        mService.addNotification(nr1);
+        mService.addNotification(summary);
+
+        // GroupHelper is a mock, so make the calls it would make
+        // Remove the app's summary notification
+        mService.removeAppSummaryLocked(summary.getKey());
+        waitForIdle();
+
+        // Add aggregate group summary
+        NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS,
+                mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN,
+                nr0.getChannel().getId());
+        NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(),
+            nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr);
+        mService.addNotification(aggregateSummary);
+
+        nr0.setOverrideGroupKey(aggregateGroupName);
+        nr1.setOverrideGroupKey(aggregateGroupName);
+        final String fullAggregateGroupKey = nr0.getGroupKey();
+
+        assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName);
+        assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo(
+                nr0.getChannel().getId());
+        assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue();
+        assertThat(mService.mSummaryByGroupKey.containsKey(originalGroupKey)).isFalse();
+
+        // Cancel the original app summary (is already removed)
+        mBinderService.cancelNotificationWithTag(summary.getSbn().getPackageName(),
+                summary.getSbn().getPackageName(), summary.getSbn().getTag(),
+                summary.getSbn().getId(), summary.getSbn().getUserId());
+        waitForIdle();
+
+        // Check if NMS.CancelNotificationRunnable calls maybeCancelGroupChildrenForCanceledSummary
+        verify(mGroupHelper, times(1)).maybeCancelGroupChildrenForCanceledSummary(
+                eq(summary.getSbn().getPackageName()), eq(summary.getSbn().getTag()),
+                eq(summary.getSbn().getId()), eq(summary.getSbn().getUserId()),
+                eq(REASON_APP_CANCEL));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testUpdateChannel_notifyGroupHelper() throws Exception {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        mTestNotificationChannel.setLightColor(Color.CYAN);
+        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
+                eq(mTestNotificationChannel.getId()), anyBoolean()))
+                .thenReturn(mTestNotificationChannel);
+
+        mBinderService.updateNotificationChannelForPackage(mPkg, mUid, mTestNotificationChannel);
+        mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME);
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onChannelUpdated(eq(Process.myUserHandle().getIdentifier()),
+                eq(mPkg), eq(mTestNotificationChannel), any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testSnoozeRunnable_snoozeAggregateGroupChild_summaryNotSnoozed() throws Exception {
+        final String aggregateGroupName = "Aggregate_Test";
+
+        // build autogroup summary notification
+        Notification.Builder nb = new Notification.Builder(mContext,
+                mTestNotificationChannel.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setGroup(aggregateGroupName)
+                .setGroupSummary(true)
+                .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true);
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1,
+                "tag" + System.currentTimeMillis(), mUid, 0, nb.build(),
+                UserHandle.getUserHandleForUid(mUid), null, 0);
+        final NotificationRecord summary = new NotificationRecord(mContext, sbn,
+                mTestNotificationChannel);
+
+        final NotificationRecord child = generateNotificationRecord(
+                mTestNotificationChannel, 2, aggregateGroupName, false);
+        mService.addNotification(summary);
+        mService.addNotification(child);
+        when(mSnoozeHelper.canSnooze(anyInt())).thenReturn(true);
+
+        // snooze child only
+        NotificationManagerService.SnoozeNotificationRunnable snoozeNotificationRunnable =
+                mService.new SnoozeNotificationRunnable(
+                    child.getKey(), 100, null);
+        snoozeNotificationRunnable.run();
+
+        // only child should be snoozed
+        verify(mSnoozeHelper, times(1)).snooze(any(NotificationRecord.class), anyLong());
+
+        // both group summary and child should be cancelled
+        assertNull(mService.getNotificationRecord(summary.getKey()));
+        assertNull(mService.getNotificationRecord(child.getKey()));
+
+        assertEquals(4, mNotificationRecordLogger.numCalls());
+        assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_SNOOZED,
+                mNotificationRecordLogger.event(0));
+        assertEquals(
+                NotificationRecordLogger.NotificationCancelledEvent.NOTIFICATION_CANCEL_SNOOZED,
+                mNotificationRecordLogger.event(1));
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testOnlyForceGroupIfNeeded_newNotification_notAutogrouped() {
+        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
+        when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(false);
+        mService.addEnqueuedNotification(r);
+        NotificationManagerService.PostNotificationRunnable runnable =
+                mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(),
+                    r.getUid(), mPostNotificationTrackerFactory.newTracker(null));
+        runnable.run();
+        waitForIdle();
+
+        mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME);
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean());
+        verify(mGroupHelper, times(1)).onNotificationPostedWithDelay(eq(r), any(), any());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testOnlyForceGroupIfNeeded_newNotification_wasAutogrouped() {
+        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
+        when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(true);
+        mService.addEnqueuedNotification(r);
+        NotificationManagerService.PostNotificationRunnable runnable =
+            mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(),
+                r.getUid(), mPostNotificationTrackerFactory.newTracker(null));
+        runnable.run();
+        waitForIdle();
+
+        mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME);
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean());
+        verify(mGroupHelper, never()).onNotificationPostedWithDelay(eq(r), any(), any());
+    }
+
+    @Test
     public void testCancelAllNotifications_IgnoreForegroundService() throws Exception {
         when(mAmi.applyForegroundServiceNotification(
                 any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY);
@@ -3653,9 +3988,11 @@
         when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
         when(mPermissionHelper.isPermissionFixed(mPkg, temp.getUserId())).thenReturn(true);
 
+        NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS,
+            mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID);
+
         NotificationRecord r = mService.createAutoGroupSummary(temp.getUserId(),
-                temp.getSbn().getPackageName(), temp.getKey(), 0, mock(Icon.class), 0,
-                VISIBILITY_PRIVATE);
+            temp.getSbn().getPackageName(), temp.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr);
 
         assertThat(r.isImportanceFixed()).isTrue();
     }
@@ -4796,6 +5133,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testSnoozeRunnable_snoozeAutoGroupChild_summaryNotSnoozed() throws Exception {
         final NotificationRecord parent = generateNotificationRecord(
                 mTestNotificationChannel, 1, GroupHelper.AUTOGROUP_KEY, true);
@@ -5659,7 +5997,7 @@
     public void testAddAutogroup_requestsSort() throws Exception {
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         mService.addNotification(r);
-        mService.addAutogroupKeyLocked(r.getKey(), true);
+        mService.addAutogroupKeyLocked(r.getKey(), "grpKey", true);
 
         verify(mRankingHandler, times(1)).requestSort();
     }
@@ -5679,7 +6017,7 @@
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         r.setOverrideGroupKey("TEST");
         mService.addNotification(r);
-        mService.addAutogroupKeyLocked(r.getKey(), true);
+        mService.addAutogroupKeyLocked(r.getKey(), "grpName", true);
 
         verify(mRankingHandler, never()).requestSort();
     }
@@ -5689,7 +6027,7 @@
     public void testAutogroupSuppressSort_noSort() throws Exception {
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         mService.addNotification(r);
-        mService.addAutogroupKeyLocked(r.getKey(), false);
+        mService.addAutogroupKeyLocked(r.getKey(), "grpName", false);
 
         verify(mRankingHandler, never()).requestSort();
     }
@@ -12688,6 +13026,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testUngroupingOngoingAutoSummary() throws Exception {
         NotificationRecord nr0 =
                 generateNotificationRecord(mTestNotificationChannel, 0);
@@ -12701,10 +13040,12 @@
         // grouphelper is a mock here, so make the calls it would make
 
         // add summary
+        NotificationAttributes attr = new NotificationAttributes(
+                GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0,
+                VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID);
         mService.addNotification(
                 mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
-                    nr1.getKey(), GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0,
-                    VISIBILITY_PRIVATE));
+                    nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
 
         // cancel both children
         mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(),
@@ -12714,7 +13055,8 @@
         waitForIdle();
 
         // group helper would send 'remove summary' event
-        mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName());
+        mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(),
+                AUTOGROUP_KEY);
         waitForIdle();
 
         // make sure the summary was removed and not re-posted
@@ -12722,6 +13064,45 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testUngroupingOngoingAutoSummary_forceGrouping() throws Exception {
+        NotificationRecord nr0 =
+            generateNotificationRecord(mTestNotificationChannel, 0);
+        NotificationRecord nr1 =
+            generateNotificationRecord(mTestNotificationChannel, 0);
+        nr1.getSbn().getNotification().flags |= FLAG_ONGOING_EVENT;
+
+        mService.addNotification(nr0);
+        mService.addNotification(nr1);
+
+        // grouphelper is a mock here, so make the calls it would make
+
+        // add summary
+        NotificationAttributes attr = new NotificationAttributes(
+            GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0,
+            VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID);
+        mService.addNotification(
+            mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
+                nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
+
+        // cancel both children
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(),
+            nr0.getSbn().getId(), nr0.getSbn().getUserId());
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1.getSbn().getTag(),
+            nr1.getSbn().getId(), nr1.getSbn().getUserId());
+        waitForIdle();
+
+        // group helper would send 'remove summary' event
+        mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(),
+            AUTOGROUP_KEY);
+        waitForIdle();
+
+        // make sure the summary was removed and not re-posted
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(0);
+    }
+
+    @Test
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testUngroupingAutoSummary_differentUsers() throws Exception {
         NotificationRecord nr0 =
                 generateNotificationRecord(mTestNotificationChannel, 0, USER_SYSTEM);
@@ -12729,11 +13110,14 @@
                 generateNotificationRecord(mTestNotificationChannel, 1, USER_SYSTEM);
 
         // add notifications + summary for USER_SYSTEM
+        NotificationAttributes attr = new NotificationAttributes(
+            GroupHelper.BASE_FLAGS, mock(Icon.class), 0,
+            VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID);
         mService.addNotification(nr0);
         mService.addNotification(nr1);
         mService.addNotification(
                 mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
-                nr1.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE));
+                nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
 
         // add notifications + summary for USER_ALL
         NotificationRecord nr0_all =
@@ -12746,7 +13130,7 @@
         mService.addNotification(
                 mService.createAutoGroupSummary(nr0_all.getUserId(),
                 nr0_all.getSbn().getPackageName(),
-                nr0_all.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE));
+                nr0_all.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
 
         // cancel both children for USER_ALL
         mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0_all.getSbn().getTag(),
@@ -12757,7 +13141,7 @@
 
         // group helper would send 'remove summary' event
         mService.clearAutogroupSummaryLocked(UserHandle.USER_ALL,
-                nr0_all.getSbn().getPackageName());
+                nr0_all.getSbn().getPackageName(), AUTOGROUP_KEY);
         waitForIdle();
 
         // make sure the right summary was removed
@@ -12770,6 +13154,58 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testUngroupingAutoSummary_differentUsers_forceGrouping() throws Exception {
+        NotificationRecord nr0 =
+            generateNotificationRecord(mTestNotificationChannel, 0, USER_SYSTEM);
+        NotificationRecord nr1 =
+            generateNotificationRecord(mTestNotificationChannel, 1, USER_SYSTEM);
+
+        // add notifications + summary for USER_SYSTEM
+        NotificationAttributes attr = new NotificationAttributes(
+            GroupHelper.BASE_FLAGS, mock(Icon.class), 0,
+            VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID);
+        mService.addNotification(nr0);
+        mService.addNotification(nr1);
+        mService.addNotification(
+            mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
+                nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
+
+        // add notifications + summary for USER_ALL
+        NotificationRecord nr0_all =
+            generateNotificationRecord(mTestNotificationChannel, 2, UserHandle.USER_ALL);
+        NotificationRecord nr1_all =
+            generateNotificationRecord(mTestNotificationChannel, 3, UserHandle.USER_ALL);
+
+        mService.addNotification(nr0_all);
+        mService.addNotification(nr1_all);
+        mService.addNotification(
+            mService.createAutoGroupSummary(nr0_all.getUserId(),
+                nr0_all.getSbn().getPackageName(),
+                nr0_all.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr));
+
+        // cancel both children for USER_ALL
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0_all.getSbn().getTag(),
+            nr0_all.getSbn().getId(), UserHandle.USER_ALL);
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1_all.getSbn().getTag(),
+            nr1_all.getSbn().getId(), UserHandle.USER_ALL);
+        waitForIdle();
+
+        // group helper would send 'remove summary' event
+        mService.clearAutogroupSummaryLocked(UserHandle.USER_ALL,
+            nr0_all.getSbn().getPackageName(), AUTOGROUP_KEY);
+        waitForIdle();
+
+        // make sure the right summary was removed
+        assertThat(mService.getNotificationCount(nr0_all.getSbn().getPackageName(),
+            UserHandle.USER_ALL, 0, null)).isEqualTo(0);
+
+        // the USER_SYSTEM notifications + summary were not removed
+        assertThat(mService.getNotificationCount(nr0.getSbn().getPackageName(),
+            USER_SYSTEM, 0, null)).isEqualTo(3);
+    }
+
+    @Test
     public void testStrongAuthTracker_isInLockDownMode() {
         mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
                 STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);