Merge "Move autogrouping into framework."
diff --git a/api/system-current.txt b/api/system-current.txt
index 7805ae3..573b1f2 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -37556,8 +37556,6 @@
     method public int getUser();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
-    field public static final java.lang.String GROUP_KEY_OVERRIDE_KEY = "group_key_override";
-    field public static final java.lang.String NEEDS_AUTOGROUPING_KEY = "autogroup_needed";
   }
 
   public final class Condition implements android.os.Parcelable {
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 530b8bb..4150172 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -134,11 +134,11 @@
         this.mLockscreenVisibility = lockscreenVisibility;
     }
 
-    // Modifiable by apps.
+    // Modifiable by apps on channel creation.
 
     /**
      * Sets the ringtone that should be played for notifications posted to this channel if
-     * the notifications don't supply a ringtone.
+     * the notifications don't supply a ringtone. Only modifiable on channel creation.
      */
     public void setDefaultRingtone(Uri defaultRingtone) {
         this.mRingtone = defaultRingtone;
@@ -146,7 +146,7 @@
 
     /**
      * Sets whether notifications posted to this channel should display notification lights,
-     * on devices that support that feature.
+     * on devices that support that feature. Only modifiable on channel creation.
      */
     public void setLights(boolean lights) {
         this.mLights = lights;
@@ -154,7 +154,7 @@
 
     /**
      * Sets whether notification posted to this channel should vibrate, even if individual
-     * notifications are marked as having vibration.
+     * notifications are marked as having vibration only modifiable on channel creation.
      */
     public void setVibration(boolean vibration) {
         this.mVibration = vibration;
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 61d0a1e..4a956c6 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -36,9 +36,6 @@
     private final Bundle mSignals;
     private final int mUser;
 
-    public static final String GROUP_KEY_OVERRIDE_KEY = "group_key_override";
-    public static final String NEEDS_AUTOGROUPING_KEY = "autogroup_needed";
-
     /**
      * Create a notification adjustment.
      *
diff --git a/packages/ExtServices/src/android/ext/services/notification/Ranker.java b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
index 2ce667c..63fc157 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Ranker.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
@@ -35,180 +35,15 @@
 import android.ext.services.R;
 
 /**
- * Class that provides an updatable ranker module for the notification manager..
+ * Class that provides an updatable ranker module for the notification manager.
  */
 public final class Ranker extends NotificationRankerService {
     private static final String TAG = "RocketRanker";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private static final int AUTOBUNDLE_AT_COUNT = 4;
-    private static final String AUTOBUNDLE_KEY = "ranker_bundle";
-
-    // Map of user : <Map of package : notification keys>. Only contains notifications that are not
-    // bundled by the app (aka no group or sort key).
-    Map<Integer, Map<String, LinkedHashSet<String>>> mUnbundledNotifications;
-
     @Override
     public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance,
             boolean user) {
-        if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
         return null;
     }
-
-    @Override
-    public void onNotificationPosted(StatusBarNotification sbn) {
-        if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
-        try {
-            List<String> notificationsToBundle = new ArrayList<>();
-            if (!sbn.isAppGroup()) {
-                // Not grouped by the app, add to the list of notifications for the app;
-                // send bundling update if app exceeds the autobundling limit.
-                synchronized (mUnbundledNotifications) {
-                    Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
-                            = mUnbundledNotifications.get(sbn.getUserId());
-                    if (unbundledNotificationsByUser == null) {
-                        unbundledNotificationsByUser = new HashMap<>();
-                    }
-                    mUnbundledNotifications.put(sbn.getUserId(), unbundledNotificationsByUser);
-                    LinkedHashSet<String> notificationsForPackage
-                            = unbundledNotificationsByUser.get(sbn.getPackageName());
-                    if (notificationsForPackage == null) {
-                        notificationsForPackage = new LinkedHashSet<>();
-                    }
-
-                    notificationsForPackage.add(sbn.getKey());
-                    unbundledNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
-
-                    if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) {
-                        for (String key : notificationsForPackage) {
-                            notificationsToBundle.add(key);
-                        }
-                    }
-                }
-                if (notificationsToBundle.size() > 0) {
-                    adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0),
-                            true, sbn.getUserId());
-                    adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true,
-                            sbn.getUserId());
-                }
-            } else {
-                // Grouped, but not by us. Send updates to unautobundle, if we bundled it.
-                maybeUnbundle(sbn, false, sbn.getUserId());
-            }
-        } catch (Exception e) {
-            Slog.e(TAG, "Failure processing new notification", e);
-        }
-    }
-
-    @Override
-    public void onNotificationRemoved(StatusBarNotification sbn) {
-        try {
-            maybeUnbundle(sbn, true, sbn.getUserId());
-        } catch (Exception e) {
-            Slog.e(TAG, "Error processing canceled notification", e);
-        }
-    }
-
-    /**
-     * Un-autobundles notifications that are now grouped by the app. Additionally cancels
-     * autobundling if the status change of this notification resulted in the loose notification
-     * count being under the limit.
-     */
-    private void maybeUnbundle(StatusBarNotification sbn, boolean notificationGone, int user) {
-        List<String> notificationsToUnAutobundle = new ArrayList<>();
-        boolean removeSummary = false;
-        synchronized (mUnbundledNotifications) {
-            Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
-                    = mUnbundledNotifications.get(sbn.getUserId());
-            if (unbundledNotificationsByUser == null || unbundledNotificationsByUser.size() == 0) {
-                return;
-            }
-            LinkedHashSet<String> notificationsForPackage
-                    = unbundledNotificationsByUser.get(sbn.getPackageName());
-            if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
-                return;
-            }
-            if (notificationsForPackage.remove(sbn.getKey())) {
-                if (!notificationGone) {
-                    // Add the current notification to the unbundling list if it still exists.
-                    notificationsToUnAutobundle.add(sbn.getKey());
-                }
-                // If the status change of this notification has brought the number of loose
-                // notifications back below the limit, remove the summary and un-autobundle.
-                if (notificationsForPackage.size() == AUTOBUNDLE_AT_COUNT - 1) {
-                    removeSummary = true;
-                    for (String key : notificationsForPackage) {
-                        notificationsToUnAutobundle.add(key);
-                    }
-                }
-            }
-        }
-        if (notificationsToUnAutobundle.size() > 0) {
-            if (removeSummary) {
-                adjustAutobundlingSummary(sbn.getPackageName(), null, false, user);
-            }
-            adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false,
-                    user);
-        }
-    }
-
-    @Override
-    public void onListenerConnected() {
-        if (DEBUG) Log.i(TAG, "CONNECTED");
-        mUnbundledNotifications = new HashMap<>();
-        for (StatusBarNotification sbn : getActiveNotifications()) {
-            onNotificationPosted(sbn);
-        }
-    }
-
-    private void adjustAutobundlingSummary(String packageName, String key, boolean summaryNeeded,
-            int user) {
-        Bundle signals = new Bundle();
-        if (summaryNeeded) {
-            signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, true);
-            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
-        } else {
-            signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false);
-        }
-        Adjustment adjustment = new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
-                getContext().getString(R.string.notification_ranker_autobundle_explanation), null,
-                user);
-        if (DEBUG) {
-            Log.i(TAG, "Summary update for: " + packageName + " "
-                    + (summaryNeeded ? "adding" : "removing"));
-        }
-        try {
-            adjustNotification(adjustment);
-        } catch (Exception e) {
-            Slog.e(TAG, "Adjustment failed", e);
-        }
-
-    }
-    private void adjustNotificationBundling(String packageName, List<String> keys, boolean bundle,
-            int user) {
-        List<Adjustment> adjustments = new ArrayList<>();
-        for (String key : keys) {
-            adjustments.add(createBundlingAdjustment(packageName, key, bundle, user));
-            if (DEBUG) Log.i(TAG, "Sending bundling adjustment for: " + key);
-        }
-        try {
-            adjustNotifications(adjustments);
-        } catch (Exception e) {
-            Slog.e(TAG, "Adjustments failed", e);
-        }
-    }
-
-    private Adjustment createBundlingAdjustment(String packageName, String key, boolean bundle,
-            int user) {
-        Bundle signals = new Bundle();
-        if (bundle) {
-            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
-        } else {
-            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
-        }
-        return new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
-                getContext().getString(R.string.notification_ranker_autobundle_explanation),
-                null, user);
-    }
-
 }
\ 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
new file mode 100644
index 0000000..8ea4909
--- /dev/null
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NotificationManagerService helper for auto-grouping notifications.
+ */
+public class GroupHelper {
+    private static final String TAG = "GroupHelper";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    protected static final int AUTOGROUP_AT_COUNT = 4;
+    protected static final String AUTOGROUP_KEY = "ranker_group";
+
+    private final Callback mCallback;
+
+    // Map of user : <Map of package : notification keys>. Only contains notifications that are not
+    // groupd by the app (aka no group or sort key).
+    Map<Integer, Map<String, LinkedHashSet<String>>> mUngroupedNotifications = new HashMap<>();
+
+    public GroupHelper(Callback callback) {;
+        mCallback = callback;
+    }
+
+    public void onNotificationPosted(StatusBarNotification sbn) {
+        if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
+        try {
+            List<String> notificationsToGroup = new ArrayList<>();
+            if (!sbn.isAppGroup()) {
+                // Not grouped by the app, add to the list of notifications for the app;
+                // send grouping update if app exceeds the autogrouping limit.
+                synchronized (mUngroupedNotifications) {
+                    Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser
+                            = mUngroupedNotifications.get(sbn.getUserId());
+                    if (ungroupedNotificationsByUser == null) {
+                        ungroupedNotificationsByUser = new HashMap<>();
+                    }
+                    mUngroupedNotifications.put(sbn.getUserId(), ungroupedNotificationsByUser);
+                    LinkedHashSet<String> notificationsForPackage
+                            = ungroupedNotificationsByUser.get(sbn.getPackageName());
+                    if (notificationsForPackage == null) {
+                        notificationsForPackage = new LinkedHashSet<>();
+                    }
+
+                    notificationsForPackage.add(sbn.getKey());
+                    ungroupedNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
+
+                    if (notificationsForPackage.size() >= AUTOGROUP_AT_COUNT) {
+                        notificationsToGroup.addAll(notificationsForPackage);
+                    }
+                }
+                if (notificationsToGroup.size() > 0) {
+                    adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(),
+                            notificationsToGroup.get(0), true);
+                    adjustNotificationBundling(notificationsToGroup, true);
+                }
+            } else {
+                // Grouped, but not by us. Send updates to un-autogroup, if we grouped it.
+                maybeUngroup(sbn, false, sbn.getUserId());
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Failure processing new notification", e);
+        }
+    }
+
+    public void onNotificationRemoved(StatusBarNotification sbn) {
+        try {
+            maybeUngroup(sbn, true, sbn.getUserId());
+        } catch (Exception e) {
+            Slog.e(TAG, "Error processing canceled notification", e);
+        }
+    }
+
+    /**
+     * Un-autogroups notifications that are now grouped by the app. Additionally cancels
+     * autogrouping if the status change of this notification resulted in the loose notification
+     * count being under the limit.
+     */
+    private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) {
+        List<String> notificationsToUnAutogroup = new ArrayList<>();
+        boolean removeSummary = false;
+        synchronized (mUngroupedNotifications) {
+            Map<String, LinkedHashSet<String>> ungroupdNotificationsByUser
+                    = mUngroupedNotifications.get(sbn.getUserId());
+            if (ungroupdNotificationsByUser == null || ungroupdNotificationsByUser.size() == 0) {
+                return;
+            }
+            LinkedHashSet<String> notificationsForPackage
+                    = ungroupdNotificationsByUser.get(sbn.getPackageName());
+            if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
+                return;
+            }
+            if (notificationsForPackage.remove(sbn.getKey())) {
+                if (!notificationGone) {
+                    // Add the current notification to the ungrouping list if it still exists.
+                    notificationsToUnAutogroup.add(sbn.getKey());
+                }
+                // If the status change of this notification has brought the number of loose
+                // notifications back below the limit, remove the summary and un-autogroup.
+                if (notificationsForPackage.size() == AUTOGROUP_AT_COUNT - 1) {
+                    removeSummary = true;
+                    for (String key : notificationsForPackage) {
+                        notificationsToUnAutogroup.add(key);
+                    }
+                }
+            }
+        }
+        if (notificationsToUnAutogroup.size() > 0) {
+            if (removeSummary) {
+                adjustAutogroupingSummary(userId, sbn.getPackageName(), null, false);
+            }
+            adjustNotificationBundling(notificationsToUnAutogroup, false);
+        }
+    }
+
+    private void adjustAutogroupingSummary(int userId, String packageName, String triggeringKey,
+            boolean summaryNeeded) {
+        if (summaryNeeded) {
+            mCallback.addAutoGroupSummary(userId, packageName, triggeringKey);
+        } else {
+            mCallback.removeAutoGroupSummary(userId, packageName);
+        }
+    }
+
+    private void adjustNotificationBundling(List<String> keys, boolean group) {
+        for (String key : keys) {
+            if (DEBUG) Log.i(TAG, "Sending grouping adjustment for: " + key + " group? " + group);
+            if (group) {
+                mCallback.addAutoGroup(key);
+            } else {
+                mCallback.removeAutoGroup(key);
+            }
+        }
+    }
+
+    protected interface Callback {
+        void addAutoGroup(String key);
+        void removeAutoGroup(String key);
+        void addAutoGroupSummary(int userId, String pkg, String triggeringKey);
+        void removeAutoGroupSummary(int user, String pkg);
+    }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 61bf3bd..4e50567 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -311,6 +311,7 @@
     private String mSystemNotificationSound;
 
     private SnoozeHelper mSnoozeHelper;
+    private GroupHelper mGroupHelper;
 
     private static class Archive {
         final int mBufferSize;
@@ -1017,6 +1018,35 @@
                 }
             }
         }, mUserProfiles);
+        mGroupHelper = new GroupHelper(new GroupHelper.Callback() {
+            @Override
+            public void addAutoGroup(String key) {
+                synchronized (mNotificationList) {
+                    addAutogroupKeyLocked(key);
+                }
+                mRankingHandler.requestSort();
+            }
+
+            @Override
+            public void removeAutoGroup(String key) {
+                synchronized (mNotificationList) {
+                    removeAutogroupKeyLocked(key);
+                }
+                mRankingHandler.requestSort();
+            }
+
+            @Override
+            public void addAutoGroupSummary(int userId, String pkg, String triggeringKey) {
+                createAutoGroupSummary(userId, pkg, triggeringKey);
+            }
+
+            @Override
+            public void removeAutoGroupSummary(int userId, String pkg) {
+                synchronized (mNotificationList) {
+                    clearAutogroupSummaryLocked(userId, pkg);
+                }
+            }
+        });
 
         final File systemDir = new File(Environment.getDataDirectory(), "system");
         mPolicyFile = new AtomicFile(new File(systemDir, "notification_policy.xml"));
@@ -2350,7 +2380,6 @@
                     mRankerServices.checkServiceTokenLocked(token);
                     applyAdjustmentLocked(adjustment);
                 }
-                maybeAddAutobundleSummary(adjustment);
                 mRankingHandler.requestSort();
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -2369,9 +2398,6 @@
                         applyAdjustmentLocked(adjustment);
                     }
                 }
-                for (Adjustment adjustment : adjustments) {
-                    maybeAddAutobundleSummary(adjustment);
-                }
                 mRankingHandler.requestSort();
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -2380,7 +2406,6 @@
     };
 
     private void applyAdjustmentLocked(Adjustment adjustment) {
-        maybeClearAutobundleSummaryLocked(adjustment);
         NotificationRecord n = mNotificationsByKey.get(adjustment.getKey());
         if (n == null) {
             return;
@@ -2390,107 +2415,97 @@
         }
         if (adjustment.getSignals() != null) {
             Bundle.setDefusable(adjustment.getSignals(), true);
-            final String autoGroupKey = adjustment.getSignals().getString(
-                    Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
-            if (autoGroupKey == null) {
-                EventLogTags.writeNotificationUnautogrouped(adjustment.getKey());
-            } else {
-                EventLogTags.writeNotificationAutogrouped(adjustment.getKey());
-            }
-            n.sbn.setOverrideGroupKey(autoGroupKey);
+            // TODO: apply signals
         }
     }
 
-    // Clears the 'fake' auto-bunding summary.
-    private void maybeClearAutobundleSummaryLocked(Adjustment adjustment) {
-        if (adjustment.getSignals() != null) {
-            Bundle.setDefusable(adjustment.getSignals(), true);
-            if (adjustment.getSignals().containsKey(Adjustment.NEEDS_AUTOGROUPING_KEY)
-                && !adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) {
-                ArrayMap<String, String> summaries =
-                        mAutobundledSummaries.get(adjustment.getUser());
-                if (summaries != null && summaries.containsKey(adjustment.getPackage())) {
-                    // Clear summary.
-                    final NotificationRecord removed = mNotificationsByKey.get(
-                            summaries.remove(adjustment.getPackage()));
-                    if (removed != null) {
-                        mNotificationList.remove(removed);
-                        cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED);
-                    }
-                }
+    private void addAutogroupKeyLocked(String key) {
+        NotificationRecord n = mNotificationsByKey.get(key);
+        if (n == null) {
+            return;
+        }
+        n.sbn.setOverrideGroupKey(GroupHelper.AUTOGROUP_KEY);
+        EventLogTags.writeNotificationAutogrouped(key);
+    }
+
+    private void removeAutogroupKeyLocked(String key) {
+        NotificationRecord n = mNotificationsByKey.get(key);
+        if (n == null) {
+            return;
+        }
+        n.sbn.setOverrideGroupKey(null);
+        EventLogTags.writeNotificationUnautogrouped(key);
+    }
+
+    // Clears the 'fake' auto-group summary.
+    private void clearAutogroupSummaryLocked(int userId, String pkg) {
+        ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
+        if (summaries != null && summaries.containsKey(pkg)) {
+            // Clear summary.
+            final NotificationRecord removed = mNotificationsByKey.get(summaries.remove(pkg));
+            if (removed != null) {
+                mNotificationList.remove(removed);
+                cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED);
             }
         }
     }
 
     // Posts a 'fake' summary for a package that has exceeded the solo-notification limit.
-    private void maybeAddAutobundleSummary(Adjustment adjustment) {
-        if (adjustment.getSignals() != null) {
-            Bundle.setDefusable(adjustment.getSignals(), true);
-            if (adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) {
-                final String newAutoBundleKey =
-                        adjustment.getSignals().getString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
-                int userId = -1;
-                NotificationRecord summaryRecord = null;
-                synchronized (mNotificationList) {
-                    NotificationRecord notificationRecord =
-                            mNotificationsByKey.get(adjustment.getKey());
-                    if (notificationRecord == null) {
-                        // The notification could have been cancelled again already. A successive
-                        // adjustment will post a summary if needed.
-                        return;
-                    }
-                    final StatusBarNotification adjustedSbn = notificationRecord.sbn;
-                    userId = adjustedSbn.getUser().getIdentifier();
-                    ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
-                    if (summaries == null) {
-                        summaries = new ArrayMap<>();
-                    }
-                    mAutobundledSummaries.put(userId, summaries);
-                    if (!summaries.containsKey(adjustment.getPackage())
-                            && newAutoBundleKey != null) {
-                        // Add summary
-                        final ApplicationInfo appInfo =
-                                adjustedSbn.getNotification().extras.getParcelable(
-                                        Notification.EXTRA_BUILDER_APPLICATION_INFO);
-                        final Bundle extras = new Bundle();
-                        extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
-                        final Notification summaryNotification =
-                                new Notification.Builder(getContext()).setSmallIcon(
-                                        adjustedSbn.getNotification().getSmallIcon())
-                                        .setGroupSummary(true)
-                                        .setGroup(newAutoBundleKey)
-                                        .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true)
-                                        .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
-                                        .setColor(adjustedSbn.getNotification().color)
-                                        .setLocalOnly(true)
-                                        .build();
-                        summaryNotification.extras.putAll(extras);
-                        Intent appIntent = getContext().getPackageManager()
-                                .getLaunchIntentForPackage(adjustment.getPackage());
-                        if (appIntent != null) {
-                            summaryNotification.contentIntent = PendingIntent.getActivityAsUser(
-                                    getContext(), 0, appIntent, 0, null,
-                                    UserHandle.of(userId));
-                        }
-                        final StatusBarNotification summarySbn =
-                                new StatusBarNotification(adjustedSbn.getPackageName(),
-                                        adjustedSbn.getOpPkg(),
-                                        Integer.MAX_VALUE, Adjustment.GROUP_KEY_OVERRIDE_KEY,
-                                        adjustedSbn.getUid(), adjustedSbn.getInitialPid(),
-                                        summaryNotification, adjustedSbn.getUser(),
-                                        newAutoBundleKey,
-                                        System.currentTimeMillis());
-                        summaryRecord = new NotificationRecord(getContext(), summarySbn,
-                                mRankingHelper.getNotificationChannel(adjustedSbn.getPackageName(),
-                                        adjustedSbn.getUid(),
-                                        adjustedSbn.getNotification().getNotificationChannel()));
-                        summaries.put(adjustment.getPackage(), summarySbn.getKey());
-                    }
-                }
-                if (summaryRecord != null) {
-                    mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
-                }
+    private void createAutoGroupSummary(int userId, String pkg, String triggeringKey) {
+        NotificationRecord summaryRecord = null;
+        synchronized (mNotificationList) {
+            NotificationRecord notificationRecord = mNotificationsByKey.get(triggeringKey);
+            if (notificationRecord == null) {
+                // The notification could have been cancelled again already. A successive
+                // adjustment will post a summary if needed.
+                return;
             }
+            final StatusBarNotification adjustedSbn = notificationRecord.sbn;
+            userId = adjustedSbn.getUser().getIdentifier();
+            ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
+            if (summaries == null) {
+                summaries = new ArrayMap<>();
+            }
+            mAutobundledSummaries.put(userId, summaries);
+            if (!summaries.containsKey(pkg)) {
+                // Add summary
+                final ApplicationInfo appInfo =
+                       adjustedSbn.getNotification().extras.getParcelable(
+                               Notification.EXTRA_BUILDER_APPLICATION_INFO);
+                final Bundle extras = new Bundle();
+                extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
+                final Notification summaryNotification =
+                        new Notification.Builder(getContext()).setSmallIcon(
+                                adjustedSbn.getNotification().getSmallIcon())
+                                .setGroupSummary(true)
+                                .setGroup(GroupHelper.AUTOGROUP_KEY)
+                                .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true)
+                                .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
+                                .setColor(adjustedSbn.getNotification().color)
+                                .setLocalOnly(true)
+                                .build();
+                summaryNotification.extras.putAll(extras);
+                Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
+                if (appIntent != null) {
+                    summaryNotification.contentIntent = PendingIntent.getActivityAsUser(
+                            getContext(), 0, appIntent, 0, null, UserHandle.of(userId));
+                }
+                final StatusBarNotification summarySbn =
+                        new StatusBarNotification(adjustedSbn.getPackageName(),
+                                adjustedSbn.getOpPkg(), Integer.MAX_VALUE,
+                                GroupHelper.AUTOGROUP_KEY, adjustedSbn.getUid(),
+                                adjustedSbn.getInitialPid(), summaryNotification,
+                                adjustedSbn.getUser(), GroupHelper.AUTOGROUP_KEY,
+                                System.currentTimeMillis());
+                summaryRecord = new NotificationRecord(getContext(), summarySbn,
+                        mRankingHelper.getNotificationChannel(adjustedSbn.getPackageName(),
+                                adjustedSbn.getUid(),
+                                adjustedSbn.getNotification().getNotificationChannel()));
+                summaries.put(pkg, summarySbn.getKey());
+            }
+        }
+        if (summaryRecord != null) {
+            mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
         }
     }
 
@@ -2690,6 +2705,12 @@
                         (r.mOriginalFlags & ~Notification.FLAG_FOREGROUND_SERVICE);
                 mRankingHelper.sort(mNotificationList);
                 mListeners.notifyPostedLocked(sbn, sbn /* oldSbn */);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mGroupHelper.onNotificationPosted(sbn);
+                    }
+                });
             }
         }
     };
@@ -2914,10 +2935,22 @@
                 if (notification.getSmallIcon() != null) {
                     StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                     mListeners.notifyPostedLocked(n, oldSbn);
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mGroupHelper.onNotificationPosted(n);
+                        }
+                    });
                 } else {
                     Slog.e(TAG, "Not posting notification without small icon: " + notification);
                     if (old != null && !old.isCanceled) {
                         mListeners.notifyRemovedLocked(n);
+                        mHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                mGroupHelper.onNotificationRemoved(n);
+                            }
+                        });
                     }
                     // ATTENTION: in a future release we will bail out here
                     // so that we do not play sounds, show lights, etc. for invalid
@@ -3525,6 +3558,12 @@
         if (r.getNotification().getSmallIcon() != null) {
             r.isCanceled = true;
             mListeners.notifyRemovedLocked(r.sbn);
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mGroupHelper.onNotificationRemoved(r.sbn);
+                }
+            });
         }
 
         final String canceledKey = r.getKey();
diff --git a/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java
new file mode 100644
index 0000000..22b674b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GroupHelperTest {
+    private @Mock GroupHelper.Callback mCallback;
+
+    private GroupHelper mGroupHelper;
+
+    private Context getContext() {
+        return InstrumentationRegistry.getTargetContext();
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mGroupHelper = new GroupHelper(mCallback);
+    }
+
+    private StatusBarNotification getSbn(String pkg, int id, String tag,
+            UserHandle user, String groupKey) {
+        Notification.Builder nb = new Notification.Builder(getContext())
+                .setContentTitle("A")
+                .setWhen(1205);
+        if (groupKey != null) {
+            nb.setGroup(groupKey);
+        }
+        return new StatusBarNotification(pkg, pkg, id, tag, 0, 0, 0, nb.build(), user);
+    }
+
+    private StatusBarNotification getSbn(String pkg, int id, String tag,
+            UserHandle user) {
+        return getSbn(pkg, id, tag, user, null);
+    }
+
+    @Test
+    public void testNoGroup_postingUnderLimit() throws Exception {
+        final String pkg = "package";
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+        }
+        verify(mCallback, never()).addAutoGroupSummary(
+                eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+        verify(mCallback, never()).addAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
+    public void testNoGroup_multiPackage() throws Exception {
+        final String pkg = "package";
+        final String pkg2 = "package2";
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+        }
+        mGroupHelper.onNotificationPosted(
+                getSbn(pkg2, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM));
+        verify(mCallback, never()).addAutoGroupSummary(
+                eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+        verify(mCallback, never()).addAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
+    public void testNoGroup_multiUser() throws Exception {
+        final String pkg = "package";
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+        }
+        mGroupHelper.onNotificationPosted(
+                getSbn(pkg, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.ALL));
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+        verify(mCallback, never()).addAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
+    public void testNoGroup_someAreGrouped() throws Exception {
+        final String pkg = "package";
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+        }
+        mGroupHelper.onNotificationPosted(
+                getSbn(pkg, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a"));
+        verify(mCallback, never()).addAutoGroupSummary(
+                eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+        verify(mCallback, never()).addAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+
+    @Test
+    public void testPostingOverLimit() throws Exception {
+        final String pkg = "package";
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT; i++) {
+            mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+        verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
+    public void testDropBelowLimitRemoveGroup() throws Exception {
+        final String pkg = "package";
+        List<StatusBarNotification> posted = new ArrayList<>();
+        for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT; i++) {
+            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            posted.add(sbn);
+            mGroupHelper.onNotificationPosted(sbn);
+        }
+        mGroupHelper.onNotificationRemoved(posted.remove(0));
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+        verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT - 1)).removeAutoGroup(anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+    }
+}