Merge "Allow the ranker to autobundle notifications." into nyc-dev
diff --git a/api/current.txt b/api/current.txt
index 22a7584..307e3f5 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -34741,6 +34741,7 @@
     method public int getImportance();
     method public java.lang.CharSequence getImportanceExplanation();
     method public java.lang.String getKey();
+    method public java.lang.String getOverrideGroupKey();
     method public int getRank();
     method public int getSuppressedVisualEffects();
     method public boolean isAmbient();
@@ -34771,13 +34772,16 @@
     method public int getId();
     method public java.lang.String getKey();
     method public android.app.Notification getNotification();
+    method public java.lang.String getOverrideGroupKey();
     method public java.lang.String getPackageName();
     method public long getPostTime();
     method public java.lang.String getTag();
     method public android.os.UserHandle getUser();
     method public deprecated int getUserId();
     method public boolean isClearable();
+    method public boolean isGroup();
     method public boolean isOngoing();
+    method public void setOverrideGroupKey(java.lang.String);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR;
   }
diff --git a/api/system-current.txt b/api/system-current.txt
index 4d99c2a..f99381c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5076,6 +5076,7 @@
     field public static final java.lang.String EXTRA_THREAD_TITLE = "android.threadTitle";
     field public static final java.lang.String EXTRA_TITLE = "android.title";
     field public static final java.lang.String EXTRA_TITLE_BIG = "android.title.big";
+    field public static final int FLAG_AUTOGROUP_SUMMARY = 1024; // 0x400
     field public static final int FLAG_AUTO_CANCEL = 16; // 0x10
     field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40
     field public static final int FLAG_GROUP_SUMMARY = 512; // 0x200
@@ -37119,6 +37120,22 @@
 
 package android.service.notification {
 
+  public final class Adjustment implements android.os.Parcelable {
+    ctor public Adjustment(java.lang.String, java.lang.String, int, android.os.Bundle, java.lang.CharSequence, android.net.Uri);
+    ctor protected Adjustment(android.os.Parcel);
+    method public int describeContents();
+    method public java.lang.CharSequence getExplanation();
+    method public int getImportance();
+    method public java.lang.String getKey();
+    method public java.lang.String getPackage();
+    method public android.net.Uri getReference();
+    method public android.os.Bundle getSignals();
+    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 class Condition implements android.os.Parcelable {
     ctor public Condition(android.net.Uri, java.lang.String, int);
     ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int, int, int);
@@ -37212,6 +37229,7 @@
     method public int getImportance();
     method public java.lang.CharSequence getImportanceExplanation();
     method public java.lang.String getKey();
+    method public java.lang.String getOverrideGroupKey();
     method public int getRank();
     method public int getSuppressedVisualEffects();
     method public boolean isAmbient();
@@ -37235,11 +37253,12 @@
 
   public abstract class NotificationRankerService extends android.service.notification.NotificationListenerService {
     ctor public NotificationRankerService();
-    method public final void adjustImportance(java.lang.String, android.service.notification.NotificationRankerService.Adjustment);
+    method public final void adjustNotification(android.service.notification.Adjustment);
+    method public final void adjustNotifications(java.util.List<android.service.notification.Adjustment>);
     method public final android.os.IBinder onBind(android.content.Intent);
     method public void onNotificationActionClick(java.lang.String, long, int);
     method public void onNotificationClick(java.lang.String, long);
-    method public abstract android.service.notification.NotificationRankerService.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean);
+    method public abstract android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean);
     method public void onNotificationRemoved(java.lang.String, long, int);
     method public void onNotificationVisibilityChanged(java.lang.String, long, boolean);
     field public static final int REASON_APP_CANCEL = 8; // 0x8
@@ -37256,14 +37275,11 @@
     field public static final int REASON_PACKAGE_CHANGED = 5; // 0x5
     field public static final int REASON_PACKAGE_SUSPENDED = 14; // 0xe
     field public static final int REASON_PROFILE_TURNED_OFF = 15; // 0xf
+    field public static final int REASON_UNAUTOBUNDLED = 16; // 0x10
     field public static final int REASON_USER_STOPPED = 6; // 0x6
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationRankerService";
   }
 
-  public class NotificationRankerService.Adjustment {
-    ctor public NotificationRankerService.Adjustment(int, java.lang.CharSequence, android.net.Uri);
-  }
-
   public class StatusBarNotification implements android.os.Parcelable {
     ctor public StatusBarNotification(java.lang.String, java.lang.String, int, java.lang.String, int, int, int, android.app.Notification, android.os.UserHandle, long);
     ctor public StatusBarNotification(android.os.Parcel);
@@ -37273,13 +37289,16 @@
     method public int getId();
     method public java.lang.String getKey();
     method public android.app.Notification getNotification();
+    method public java.lang.String getOverrideGroupKey();
     method public java.lang.String getPackageName();
     method public long getPostTime();
     method public java.lang.String getTag();
     method public android.os.UserHandle getUser();
     method public deprecated int getUserId();
     method public boolean isClearable();
+    method public boolean isGroup();
     method public boolean isOngoing();
+    method public void setOverrideGroupKey(java.lang.String);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR;
   }
diff --git a/api/test-current.txt b/api/test-current.txt
index eee159d..86dcd5c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -34814,6 +34814,7 @@
     method public int getImportance();
     method public java.lang.CharSequence getImportanceExplanation();
     method public java.lang.String getKey();
+    method public java.lang.String getOverrideGroupKey();
     method public int getRank();
     method public int getSuppressedVisualEffects();
     method public boolean isAmbient();
@@ -34844,13 +34845,16 @@
     method public int getId();
     method public java.lang.String getKey();
     method public android.app.Notification getNotification();
+    method public java.lang.String getOverrideGroupKey();
     method public java.lang.String getPackageName();
     method public long getPostTime();
     method public java.lang.String getTag();
     method public android.os.UserHandle getUser();
     method public deprecated int getUserId();
     method public boolean isClearable();
+    method public boolean isGroup();
     method public boolean isOngoing();
+    method public void setOverrideGroupKey(java.lang.String);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR;
   }
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 7a69c62..ee80ec3 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -25,6 +25,7 @@
 import android.content.pm.ParceledListSlice;
 import android.net.Uri;
 import android.os.Bundle;
+import android.service.notification.Adjustment;
 import android.service.notification.Condition;
 import android.service.notification.IConditionListener;
 import android.service.notification.IConditionProvider;
@@ -80,7 +81,8 @@
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
     void setInterruptionFilter(String pkg, int interruptionFilter);
 
-    void setImportanceFromRankerService(in INotificationListener token, String key, int importance, CharSequence explanation);
+    void applyAdjustmentFromRankerService(in INotificationListener token, in Adjustment adjustment);
+    void applyAdjustmentsFromRankerService(in INotificationListener token, in List<Adjustment> adjustments);
 
     ComponentName getEffectsSuppressor();
     boolean matchesCallFilter(in Bundle extras);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index faefc9d..4bf1aa3 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -21,6 +21,7 @@
 import android.annotation.IntDef;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -496,6 +497,15 @@
      */
     public static final int FLAG_GROUP_SUMMARY      = 0x00000200;
 
+    /**
+     * Bit to be bitswise-ored into the {@link #flags} field that should be
+     * set if this notification is the group summary for an auto-group of notifications.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int FLAG_AUTOGROUP_SUMMARY  = 0x00000400;
+
     public int flags;
 
     /** @hide */
@@ -1945,13 +1955,9 @@
      * @hide
      */
     public static void addFieldsFromContext(Context context, Notification notification) {
-        if (notification.extras.getParcelable(EXTRA_BUILDER_APPLICATION_INFO) == null) {
-            notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO,
-                    context.getApplicationInfo());
-        }
-        if (!notification.extras.containsKey(EXTRA_ORIGINATING_USERID)) {
-            notification.extras.putInt(EXTRA_ORIGINATING_USERID, context.getUserId());
-        }
+        notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO,
+                context.getApplicationInfo());
+        notification.extras.putInt(EXTRA_ORIGINATING_USERID, context.getUserId());
     }
 
     @Override
@@ -3020,12 +3026,13 @@
         /**
          * @hide
          */
-        public void setFlag(int mask, boolean value) {
+        public Builder setFlag(int mask, boolean value) {
             if (value) {
                 mN.flags |= mask;
             } else {
                 mN.flags &= ~mask;
             }
+            return this;
         }
 
         /**
diff --git a/core/java/android/service/notification/Adjustment.aidl b/core/java/android/service/notification/Adjustment.aidl
new file mode 100644
index 0000000..8bd814a
--- /dev/null
+++ b/core/java/android/service/notification/Adjustment.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.service.notification;
+
+parcelable Adjustment;
\ No newline at end of file
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
new file mode 100644
index 0000000..2e4f48d
--- /dev/null
+++ b/core/java/android/service/notification/Adjustment.java
@@ -0,0 +1,150 @@
+/*
+ * 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 android.service.notification;
+
+import android.annotation.SystemApi;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Ranking updates from the Ranker.
+ *
+ * @hide
+ */
+@SystemApi
+public final class Adjustment implements Parcelable {
+    private final String mPackage;
+    private final String mKey;
+    private final int mImportance;
+    private final CharSequence mExplanation;
+    private final Uri mReference;
+    private final Bundle mSignals;
+
+    public static final String GROUP_KEY_OVERRIDE_KEY = "group_key_override";
+    public static final String NEEDS_AUTOGROUPING_KEY = "autogroup_needed";
+
+    /**
+     * Create a notification adjustment.
+     *
+     * @param pkg The package of the notification.
+     * @param key The notification key.
+     * @param importance The recommended importance of the notification.
+     * @param signals A bundle of signals that should inform notification grouping and ordering.
+     * @param explanation A human-readable justification for the adjustment.
+     * @param reference A reference to an external object that augments the
+     *                  explanation, such as a
+     *                  {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI},
+     *                  or null.
+     */
+    public Adjustment(String pkg, String key, int importance, Bundle signals,
+            CharSequence explanation, Uri reference) {
+        mPackage = pkg;
+        mKey = key;
+        mImportance = importance;
+        mSignals = signals;
+        mExplanation = explanation;
+        mReference = reference;
+    }
+
+    protected Adjustment(Parcel in) {
+        if (in.readInt() == 1) {
+            mPackage = in.readString();
+        } else {
+            mPackage = null;
+        }
+        if (in.readInt() == 1) {
+            mKey = in.readString();
+        } else {
+            mKey = null;
+        }
+        mImportance = in.readInt();
+        if (in.readInt() == 1) {
+            mExplanation = in.readCharSequence();
+        } else {
+            mExplanation = null;
+        }
+        mReference = in.readParcelable(Uri.class.getClassLoader());
+        mSignals = in.readBundle();
+    }
+
+    public static final Creator<Adjustment> CREATOR = new Creator<Adjustment>() {
+        @Override
+        public Adjustment createFromParcel(Parcel in) {
+            return new Adjustment(in);
+        }
+
+        @Override
+        public Adjustment[] newArray(int size) {
+            return new Adjustment[size];
+        }
+    };
+
+    public String getPackage() {
+        return mPackage;
+    }
+
+    public String getKey() {
+        return mKey;
+    }
+
+    public int getImportance() {
+        return mImportance;
+    }
+
+    public CharSequence getExplanation() {
+        return mExplanation;
+    }
+
+    public Uri getReference() {
+        return mReference;
+    }
+
+    public Bundle getSignals() {
+        return mSignals;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        if (mPackage != null) {
+            dest.writeInt(1);
+            dest.writeString(mPackage);
+        } else {
+            dest.writeInt(0);
+        }
+        if (mKey != null) {
+            dest.writeInt(1);
+            dest.writeString(mKey);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeInt(mImportance);
+        if (mExplanation != null) {
+            dest.writeInt(1);
+            dest.writeCharSequence(mExplanation);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeParcelable(mReference, flags);
+        dest.writeBundle(mSignals);
+    }
+}
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 7325aef..a3f5224 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -1052,6 +1052,8 @@
         private int mSuppressedVisualEffects;
         private @Importance int mImportance;
         private CharSequence mImportanceExplanation;
+        // System specified group key.
+        private String mOverrideGroupKey;
 
         public Ranking() {}
 
@@ -1130,9 +1132,17 @@
             return mImportanceExplanation;
         }
 
+        /**
+         * If the system has overriden the group key, then this will be non-null, and this
+         * key should be used to bundle notifications.
+         */
+        public String getOverrideGroupKey() {
+            return mOverrideGroupKey;
+        }
+
         private void populate(String key, int rank, boolean matchesInterruptionFilter,
                 int visibilityOverride, int suppressedVisualEffects, int importance,
-                CharSequence explanation) {
+                CharSequence explanation, String overrideGroupKey) {
             mKey = key;
             mRank = rank;
             mIsAmbient = importance < IMPORTANCE_LOW;
@@ -1141,6 +1151,7 @@
             mSuppressedVisualEffects = suppressedVisualEffects;
             mImportance = importance;
             mImportanceExplanation = explanation;
+            mOverrideGroupKey = overrideGroupKey;
         }
 
         /**
@@ -1184,6 +1195,7 @@
         private ArrayMap<String, Integer> mSuppressedVisualEffects;
         private ArrayMap<String, Integer> mImportance;
         private ArrayMap<String, String> mImportanceExplanation;
+        private ArrayMap<String, String> mOverrideGroupKeys;
 
         private RankingMap(NotificationRankingUpdate rankingUpdate) {
             mRankingUpdate = rankingUpdate;
@@ -1210,7 +1222,7 @@
             int rank = getRank(key);
             outRanking.populate(key, rank, !isIntercepted(key),
                     getVisibilityOverride(key), getSuppressedVisualEffects(key),
-                    getImportance(key), getImportanceExplanation(key));
+                    getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key));
             return rank >= 0;
         }
 
@@ -1281,6 +1293,15 @@
             return mImportanceExplanation.get(key);
         }
 
+        private String getOverrideGroupKey(String key) {
+            synchronized (this) {
+                if (mOverrideGroupKeys == null) {
+                    buildOverrideGroupKeys();
+                }
+            }
+            return mOverrideGroupKeys.get(key);
+        }
+
         // Locked by 'this'
         private void buildRanksLocked() {
             String[] orderedKeys = mRankingUpdate.getOrderedKeys();
@@ -1335,6 +1356,15 @@
             }
         }
 
+        // Locked by 'this'
+        private void buildOverrideGroupKeys() {
+            Bundle overrideGroupKeys = mRankingUpdate.getOverrideGroupKeys();
+            mOverrideGroupKeys = new ArrayMap<>(overrideGroupKeys.size());
+            for (String key: overrideGroupKeys.keySet()) {
+                mOverrideGroupKeys.put(key, overrideGroupKeys.getString(key));
+            }
+        }
+
         // ----------- Parcelable
 
         @Override
diff --git a/core/java/android/service/notification/NotificationRankerService.java b/core/java/android/service/notification/NotificationRankerService.java
index 47fdac6..ee5361a 100644
--- a/core/java/android/service/notification/NotificationRankerService.java
+++ b/core/java/android/service/notification/NotificationRankerService.java
@@ -22,14 +22,19 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.Log;
 import com.android.internal.os.SomeArgs;
 
+import java.util.List;
+
 /**
  * A service that helps the user manage notifications. This class is only used to
  * extend the framework service and may not be implemented by non-framework components.
@@ -91,27 +96,8 @@
     /** Notification was canceled by the owning managed profile being turned off. */
     public static final int REASON_PROFILE_TURNED_OFF = 15;
 
-    public class Adjustment {
-        int mImportance;
-        CharSequence mExplanation;
-        Uri mReference;
-
-        /**
-         * Create a notification importance adjustment.
-         *
-         * @param importance The final importance of the notification.
-         * @param explanation A human-readable justification for the adjustment.
-         * @param reference A reference to an external object that augments the
-         *                  explanation, such as a
-         *                  {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI},
-         *                  or null.
-         */
-        public Adjustment(int importance, CharSequence explanation, Uri reference) {
-            mImportance = importance;
-            mExplanation = explanation;
-            mReference = reference;
-        }
-    }
+    /** Autobundled summary notification was canceled because its group was unbundled */
+    public static final int REASON_UNAUTOBUNDLED = 16;
 
     private Handler mHandler;
 
@@ -200,18 +186,32 @@
     }
 
     /**
-     * Change the importance of an existing notification.  N.B. this won’t cause
+     * Updates a notification.  N.B. this won’t cause
      * an existing notification to alert, but might allow a future update to
      * this notification to alert.
      *
-     * @param key the notification key
-     * @param adjustment the new importance with an explanation
+     * @param adjustment the adjustment with an explanation
      */
-    public final void adjustImportance(String key, Adjustment adjustment) {
+    public final void adjustNotification(Adjustment adjustment) {
         if (!isBound()) return;
         try {
-            getNotificationInterface().setImportanceFromRankerService(mWrapper, key,
-                    adjustment.mImportance, adjustment.mExplanation);
+            getNotificationInterface().applyAdjustmentFromRankerService(mWrapper, adjustment);
+        } catch (android.os.RemoteException ex) {
+            Log.v(TAG, "Unable to contact notification manager", ex);
+        }
+    }
+
+    /**
+     * Updates existing notifications. Re-ranking won't occur until all adjustments are applied.
+     * N.B. this won’t cause an existing notification to alert, but might allow a future update to
+     * these notifications to alert.
+     *
+     * @param adjustments a list of adjustments with explanations
+     */
+    public final void adjustNotifications(List<Adjustment> adjustments) {
+        if (!isBound()) return;
+        try {
+            getNotificationInterface().applyAdjustmentsFromRankerService(mWrapper, adjustments);
         } catch (android.os.RemoteException ex) {
             Log.v(TAG, "Unable to contact notification manager", ex);
         }
@@ -299,7 +299,7 @@
                     args.recycle();
                     Adjustment adjustment = onNotificationEnqueued(sbn, importance, user);
                     if (adjustment != null) {
-                        adjustImportance(sbn.getKey(), adjustment);
+                        adjustNotification(adjustment);
                     }
                 } break;
 
diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java
index 79f6fc4..788b5c0 100644
--- a/core/java/android/service/notification/NotificationRankingUpdate.java
+++ b/core/java/android/service/notification/NotificationRankingUpdate.java
@@ -30,16 +30,18 @@
     private final Bundle mSuppressedVisualEffects;
     private final int[] mImportance;
     private final Bundle mImportanceExplanation;
+    private final Bundle mOverrideGroupKeys;
 
     public NotificationRankingUpdate(String[] keys, String[] interceptedKeys,
             Bundle visibilityOverrides, Bundle suppressedVisualEffects,
-            int[] importance, Bundle explanation) {
+            int[] importance, Bundle explanation, Bundle overrideGroupKeys) {
         mKeys = keys;
         mInterceptedKeys = interceptedKeys;
         mVisibilityOverrides = visibilityOverrides;
         mSuppressedVisualEffects = suppressedVisualEffects;
         mImportance = importance;
         mImportanceExplanation = explanation;
+        mOverrideGroupKeys = overrideGroupKeys;
     }
 
     public NotificationRankingUpdate(Parcel in) {
@@ -50,6 +52,7 @@
         mImportance = new int[mKeys.length];
         in.readIntArray(mImportance);
         mImportanceExplanation = in.readBundle();
+        mOverrideGroupKeys = in.readBundle();
     }
 
     @Override
@@ -65,6 +68,7 @@
         out.writeBundle(mSuppressedVisualEffects);
         out.writeIntArray(mImportance);
         out.writeBundle(mImportanceExplanation);
+        out.writeBundle(mOverrideGroupKeys);
     }
 
     public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR
@@ -101,4 +105,8 @@
     public Bundle getImportanceExplanation() {
         return mImportanceExplanation;
     }
+
+    public Bundle getOverrideGroupKeys() {
+        return mOverrideGroupKeys;
+    }
 }
diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java
index 198e43d..0221b66 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -33,7 +33,8 @@
     private final int id;
     private final String tag;
     private final String key;
-    private final String groupKey;
+    private String groupKey;
+    private String overrideGroupKey;
 
     private final int uid;
     private final String opPkg;
@@ -51,6 +52,27 @@
                 System.currentTimeMillis());
     }
 
+    /** @hide */
+    public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid,
+            int initialPid, Notification notification, UserHandle user, String overrideGroupKey,
+            long postTime) {
+        if (pkg == null) throw new NullPointerException();
+        if (notification == null) throw new NullPointerException();
+
+        this.pkg = pkg;
+        this.opPkg = opPkg;
+        this.id = id;
+        this.tag = tag;
+        this.uid = uid;
+        this.initialPid = initialPid;
+        this.notification = notification;
+        this.user = user;
+        this.postTime = postTime;
+        this.overrideGroupKey = overrideGroupKey;
+        this.key = key();
+        this.groupKey = groupKey();
+    }
+
     public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid,
             int initialPid, int score, Notification notification, UserHandle user,
             long postTime) {
@@ -84,15 +106,27 @@
         this.notification = new Notification(in);
         this.user = UserHandle.readFromParcel(in);
         this.postTime = in.readLong();
+        if (in.readInt() != 0) {
+            this.overrideGroupKey = in.readString();
+        } else {
+            this.overrideGroupKey = null;
+        }
         this.key = key();
         this.groupKey = groupKey();
     }
 
     private String key() {
-        return user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
+        String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
+        if (overrideGroupKey != null && getNotification().isGroupSummary()) {
+            sbnKey = sbnKey + "|" + overrideGroupKey;
+        }
+        return sbnKey;
     }
 
     private String groupKey() {
+        if (overrideGroupKey != null) {
+            return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey;
+        }
         final String group = getNotification().getGroup();
         final String sortKey = getNotification().getSortKey();
         if (group == null && sortKey == null) {
@@ -105,6 +139,17 @@
                         : "g:" + group);
     }
 
+    /**
+     * Returns true if this notification is part of a group.
+     */
+    public boolean isGroup() {
+        if (overrideGroupKey != null || getNotification().getGroup() != null
+                || getNotification().getSortKey() != null) {
+            return true;
+        }
+        return false;
+    }
+
     public void writeToParcel(Parcel out, int flags) {
         out.writeString(this.pkg);
         out.writeString(this.opPkg);
@@ -121,6 +166,12 @@
         user.writeToParcel(out, flags);
 
         out.writeLong(this.postTime);
+        if (this.overrideGroupKey != null) {
+            out.writeInt(1);
+            out.writeString(this.overrideGroupKey);
+        } else {
+            out.writeInt(0);
+        }
     }
 
     public int describeContents() {
@@ -149,22 +200,22 @@
         this.notification.cloneInto(no, false); // light copy
         return new StatusBarNotification(this.pkg, this.opPkg,
                 this.id, this.tag, this.uid, this.initialPid,
-                0, no, this.user, this.postTime);
+                no, this.user, this.overrideGroupKey, this.postTime);
     }
 
     @Override
     public StatusBarNotification clone() {
         return new StatusBarNotification(this.pkg, this.opPkg,
                 this.id, this.tag, this.uid, this.initialPid,
-                0, this.notification.clone(), this.user, this.postTime);
+                this.notification.clone(), this.user, this.overrideGroupKey, this.postTime);
     }
 
     @Override
     public String toString() {
         return String.format(
-                "StatusBarNotification(pkg=%s user=%s id=%d tag=%s score=%d key=%s: %s)",
+                "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)",
                 this.pkg, this.user, this.id, this.tag,
-                0, this.key, this.notification);
+                this.key, this.notification);
     }
 
     /** Convenience method to check the notification's flags for
@@ -258,6 +309,21 @@
     }
 
     /**
+     * Sets the override group key.
+     */
+    public void setOverrideGroupKey(String overrideGroupKey) {
+        this.overrideGroupKey = overrideGroupKey;
+        groupKey = groupKey();
+    }
+
+    /**
+     * Returns the override group key.
+     */
+    public String getOverrideGroupKey() {
+        return overrideGroupKey;
+    }
+
+    /**
      * @hide
      */
     public Context getPackageContext(Context context) {
diff --git a/packages/ExtServices/res/values/strings.xml b/packages/ExtServices/res/values/strings.xml
index 0763403..b77ff10 100644
--- a/packages/ExtServices/res/values/strings.xml
+++ b/packages/ExtServices/res/values/strings.xml
@@ -17,4 +17,5 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name">Android Services Library</string>
     <string name="notification_ranker">Android Notification Ranking Service</string>
+    <string name="notification_ranker_autobundle_explanation">Auto-grouping updated by Ranking Service</string>
 </resources>
diff --git a/packages/ExtServices/src/android/ext/services/notification/Ranker.java b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
index 0b2b1a4..3ef2aea 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Ranker.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
@@ -16,16 +16,36 @@
 
 package android.ext.services.notification;
 
+import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
+
+import android.os.Bundle;
+import android.service.notification.Adjustment;
 import android.service.notification.NotificationRankerService;
 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;
+
+import android.ext.services.R;
 
 /**
  * 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 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 package : notification keys. Only contains notifications that are not bundled
+    // by the app (aka no group or sort key).
+    Map<String, LinkedHashSet<String>> mUnbundledNotifications;
 
     @Override
     public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance,
@@ -37,10 +57,146 @@
     @Override
     public void onNotificationPosted(StatusBarNotification sbn) {
         if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
+        try {
+            List<String> notificationsToBundle = new ArrayList<>();
+            if (!sbn.isGroup()) {
+                // 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) {
+                    LinkedHashSet<String> notificationsForPackage
+                            = mUnbundledNotifications.get(sbn.getPackageName());
+                    if (notificationsForPackage == null) {
+                        notificationsForPackage = new LinkedHashSet<>();
+                    }
+                    if (notificationsForPackage.contains(sbn.getKey())) {
+                        return;
+                    }
+                    notificationsForPackage.add(sbn.getKey());
+                    mUnbundledNotifications.put(sbn.getPackageName(), notificationsForPackage);
+
+                    if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) {
+                        // Autobundle all but the most recently posted (not updated) notification.
+                        int count = 0;
+                        for (String key : notificationsForPackage) {
+                            if (count < notificationsForPackage.size() - 1) {
+                                notificationsToBundle.add(key);
+                            }
+                            count++;
+                        }
+                    }
+                }
+                if (notificationsToBundle.size() > 0) {
+                    adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0),
+                            true);
+                    adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true);
+                }
+            } else {
+                // Grouped, but not by us. Send updates to unautobundle, if we bundled it.
+                maybeUnbundle(sbn, false);
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Failure processing new notification", e);
+        }
+    }
+
+    @Override
+    public void onNotificationRemoved(StatusBarNotification sbn) {
+        try {
+            maybeUnbundle(sbn, true);
+        } 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) {
+        List<String> notificationsToUnAutobundle = new ArrayList<>();
+        boolean removeSummary = false;
+        synchronized (mUnbundledNotifications) {
+            LinkedHashSet<String> notificationsForPackage
+                    = mUnbundledNotifications.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);
+            }
+            adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false);
+        }
     }
 
     @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) {
+        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);
+        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) {
+        List<Adjustment> adjustments = new ArrayList<>();
+        for (String key : keys) {
+            adjustments.add(createBundlingAdjustment(packageName, key, bundle));
+            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) {
+        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);
+    }
+
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index c9fe2bd..6570221 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -34,6 +34,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -257,16 +258,21 @@
     }
 
     public void add(Entry entry, RankingMap ranking) {
-        mEntries.put(entry.notification.getKey(), entry);
-        updateRankingAndSort(ranking);
+        synchronized (mEntries) {
+            mEntries.put(entry.notification.getKey(), entry);
+        }
         mGroupManager.onEntryAdded(entry);
+        updateRankingAndSort(ranking);
     }
 
     public Entry remove(String key, RankingMap ranking) {
-        Entry removed = mEntries.remove(key);
+        Entry removed = null;
+        synchronized (mEntries) {
+            removed = mEntries.remove(key);
+        }
         if (removed == null) return null;
-        updateRankingAndSort(ranking);
         mGroupManager.onEntryRemoved(removed);
+        updateRankingAndSort(ranking);
         return removed;
     }
 
@@ -316,9 +322,30 @@
         return Ranking.IMPORTANCE_UNSPECIFIED;
     }
 
+    public String getOverrideGroupKey(String key) {
+        if (mRankingMap != null) {
+            mRankingMap.getRanking(key, mTmpRanking);
+            return mTmpRanking.getOverrideGroupKey();
+        }
+         return null;
+    }
+
     private void updateRankingAndSort(RankingMap ranking) {
         if (ranking != null) {
             mRankingMap = ranking;
+            synchronized (mEntries) {
+                final int N = mEntries.size();
+                for (int i = 0; i < N; i++) {
+                    Entry entry = mEntries.valueAt(i);
+                    final StatusBarNotification oldSbn = entry.notification.clone();
+                    final String overrideGroupKey = getOverrideGroupKey(entry.key);
+                    if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
+                        entry.notification.setOverrideGroupKey(overrideGroupKey);
+                        mGroupManager.onEntryUpdated(entry, oldSbn);
+                    }
+                    //mGroupManager.onEntryBundlingUpdated(entry, getOverrideGroupKey(entry.key));
+                }
+            }
         }
         filterAndSort();
     }
@@ -328,16 +355,18 @@
     public void filterAndSort() {
         mSortedAndFiltered.clear();
 
-        final int N = mEntries.size();
-        for (int i = 0; i < N; i++) {
-            Entry entry = mEntries.valueAt(i);
-            StatusBarNotification sbn = entry.notification;
+        synchronized (mEntries) {
+            final int N = mEntries.size();
+            for (int i = 0; i < N; i++) {
+                Entry entry = mEntries.valueAt(i);
+                StatusBarNotification sbn = entry.notification;
 
-            if (shouldFilterOut(sbn)) {
-                continue;
+                if (shouldFilterOut(sbn)) {
+                    continue;
+                }
+
+                mSortedAndFiltered.add(entry);
             }
-
-            mSortedAndFiltered.add(entry);
         }
 
         Collections.sort(mSortedAndFiltered, mRankingComparator);
@@ -398,16 +427,17 @@
             NotificationData.Entry e = mSortedAndFiltered.get(active);
             dumpEntry(pw, indent, active, e);
         }
-
-        int M = mEntries.size();
-        pw.print(indent);
-        pw.println("inactive notifications: " + (M - active));
-        int inactiveCount = 0;
-        for (int i = 0; i < M; i++) {
-            Entry entry = mEntries.valueAt(i);
-            if (!mSortedAndFiltered.contains(entry)) {
-                dumpEntry(pw, indent, inactiveCount, entry);
-                inactiveCount++;
+        synchronized (mEntries) {
+            int M = mEntries.size();
+            pw.print(indent);
+            pw.println("inactive notifications: " + (M - active));
+            int inactiveCount = 0;
+            for (int i = 0; i < M; i++) {
+                Entry entry = mEntries.valueAt(i);
+                if (!mSortedAndFiltered.contains(entry)) {
+                    dumpEntry(pw, indent, inactiveCount, entry);
+                    inactiveCount++;
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index f7a6b271..a27ec28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -26,6 +26,7 @@
 
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Objects;
 
 /**
  * A class to handle notifications and their corresponding groups.
@@ -121,6 +122,15 @@
         }
     }
 
+    public void onEntryBundlingUpdated(final NotificationData.Entry updated,
+            final String overrideGroupKey) {
+        final StatusBarNotification oldSbn = updated.notification.clone();
+        if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
+            updated.notification.setOverrideGroupKey(overrideGroupKey);
+            onEntryUpdated(updated, oldSbn);
+        }
+    }
+
     private void updateSuppression(NotificationGroup group) {
         if (group == null) {
             return;
@@ -129,7 +139,7 @@
         group.suppressed = group.summary != null && !group.expanded
                 && (group.children.size() == 1
                 || (group.children.size() == 0
-                        && !group.summary.notification.getNotification().isGroupChild()
+                        && group.summary.notification.getNotification().isGroupSummary()
                         && hasIsolatedChildren(group)));
         if (prevSuppressed != group.suppressed) {
             mListener.onGroupsChanged();
@@ -173,7 +183,7 @@
 
     public boolean isOnlyChildInSuppressedGroup(StatusBarNotification sbn) {
         return isGroupSuppressed(sbn.getGroupKey())
-                && sbn.getNotification().isGroupChild()
+                && !sbn.getNotification().isGroupSummary()
                 && getTotalNumberOfChildren(sbn) == 1;
     }
 
@@ -278,11 +288,12 @@
         }
         return sbn.getNotification().isGroupSummary();
     }
+
     private boolean isGroupChild(StatusBarNotification sbn) {
         if (isIsolated(sbn)) {
             return false;
         }
-        return sbn.getNotification().isGroupChild();
+        return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
     }
 
     private String getGroupKey(StatusBarNotification sbn) {
@@ -335,7 +346,7 @@
 
     private boolean shouldIsolate(StatusBarNotification sbn) {
         NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
-        return sbn.getNotification().isGroupChild()
+        return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
                 && (sbn.getNotification().fullScreenIntent != null
                         || notificationGroup == null
                         || !notificationGroup.expanded
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 99c41ea..c855276 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -30,6 +30,7 @@
 import static android.service.notification.NotificationRankerService.REASON_PACKAGE_CHANGED;
 import static android.service.notification.NotificationRankerService.REASON_PACKAGE_SUSPENDED;
 import static android.service.notification.NotificationRankerService.REASON_PROFILE_TURNED_OFF;
+import static android.service.notification.NotificationRankerService.REASON_UNAUTOBUNDLED;
 import static android.service.notification.NotificationRankerService.REASON_USER_STOPPED;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
 import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF;
@@ -39,8 +40,6 @@
 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_DEFAULT;
 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_NONE;
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.END_TAG;
-import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 import android.Manifest;
 import android.annotation.Nullable;
@@ -97,6 +96,7 @@
 import android.os.UserManager;
 import android.os.Vibrator;
 import android.provider.Settings;
+import android.service.notification.Adjustment;
 import android.service.notification.Condition;
 import android.service.notification.IConditionProvider;
 import android.service.notification.INotificationListener;
@@ -136,7 +136,6 @@
 
 import libcore.io.IoUtils;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.xmlpull.v1.XmlPullParser;
@@ -158,7 +157,6 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -263,6 +261,7 @@
             new ArrayList<NotificationRecord>();
     final ArrayMap<String, NotificationRecord> mNotificationsByKey =
             new ArrayMap<String, NotificationRecord>();
+    final ArrayMap<String, String> mAutobundledSummaries = new ArrayMap<>();
     final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
     final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
     final PolicyAccess mPolicyAccess = new PolicyAccess();
@@ -283,11 +282,6 @@
     private static final String TAG_NOTIFICATION_POLICY = "notification-policy";
     private static final String ATTR_VERSION = "version";
 
-    // Obsolete:  converted if present, but not resaved to disk.
-    private static final String TAG_BLOCKED_PKGS = "blocked-packages";
-    private static final String TAG_PACKAGE = "package";
-    private static final String ATTR_NAME = "name";
-
     private RankingHelper mRankingHelper;
 
     private final UserProfiles mUserProfiles = new UserProfiles();
@@ -1259,10 +1253,13 @@
             checkCallerIsSystemOrSameApp(pkg);
             userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
                     Binder.getCallingUid(), userId, true, false, "cancelNotificationWithTag", pkg);
-            // Don't allow client applications to cancel foreground service notis.
+            // Don't allow client applications to cancel foreground service notis or autobundled
+            // summaries.
             cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), pkg, tag, id, 0,
-                    Binder.getCallingUid() == Process.SYSTEM_UID
-                            ? 0 : Notification.FLAG_FOREGROUND_SERVICE, false, userId,
+                    (Binder.getCallingUid() == Process.SYSTEM_UID
+                            ? 0 : Notification.FLAG_FOREGROUND_SERVICE)
+                            | (Binder.getCallingUid() == Process.SYSTEM_UID
+                            ? 0 : Notification.FLAG_AUTOGROUP_SUMMARY), false, userId,
                     REASON_APP_CANCEL, null);
         }
 
@@ -1404,7 +1401,9 @@
                 final int N = mNotificationList.size();
                 for (int i = 0; i < N; i++) {
                     final StatusBarNotification sbn = mNotificationList.get(i).sbn;
-                    if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId) {
+                    if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId
+                            && (sbn.getNotification().flags
+                            & Notification.FLAG_AUTOGROUP_SUMMARY) != 0) {
                         // We could pass back a cloneLight() but clients might get confused and
                         // try to send this thing back to notify() again, which would not work
                         // very well.
@@ -1519,7 +1518,8 @@
             checkCallerIsSystemOrSameApp(component.getPackageName());
             long identity = Binder.clearCallingIdentity();
             try {
-                ManagedServices manager = mRankerServices.isComponentEnabledForCurrentProfiles(component)
+                ManagedServices manager =
+                        mRankerServices.isComponentEnabledForCurrentProfiles(component)
                         ? mRankerServices
                         : mListeners;
                 manager.setComponentState(component, true);
@@ -2035,25 +2035,123 @@
         }
 
         @Override
-        public void setImportanceFromRankerService(INotificationListener token, String key,
-                int importance, CharSequence explanation) throws RemoteException {
-            if (importance == IMPORTANCE_NONE) {
-                throw new IllegalArgumentException("blocking not allowed: key=" + key);
-            }
+        public void applyAdjustmentFromRankerService(INotificationListener token,
+                Adjustment adjustment) throws RemoteException {
             final long identity = Binder.clearCallingIdentity();
             try {
                 synchronized (mNotificationList) {
                     mRankerServices.checkServiceTokenLocked(token);
-                    NotificationRecord n = mNotificationsByKey.get(key);
-                    n.setImportance(importance, explanation);
-                    mRankingHandler.requestSort();
+                    applyAdjustmentLocked(adjustment);
                 }
+                maybeAddAutobundleSummary(adjustment);
+                mRankingHandler.requestSort();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void applyAdjustmentsFromRankerService(INotificationListener token,
+                List<Adjustment> adjustments) throws RemoteException {
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mNotificationList) {
+                    mRankerServices.checkServiceTokenLocked(token);
+                    for (Adjustment adjustment : adjustments) {
+                        applyAdjustmentLocked(adjustment);
+                    }
+                }
+                for (Adjustment adjustment : adjustments) {
+                    maybeAddAutobundleSummary(adjustment);
+                }
+                mRankingHandler.requestSort();
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
     };
 
+    private void applyAdjustmentLocked(Adjustment adjustment) {
+        maybeClearAutobundleSummaryLocked(adjustment);
+        NotificationRecord n = mNotificationsByKey.get(adjustment.getKey());
+        if (n == null) {
+            return;
+        }
+        if (adjustment.getImportance() != IMPORTANCE_NONE) {
+            n.setImportance(adjustment.getImportance(), adjustment.getExplanation());
+        }
+        if (adjustment.getSignals() != null) {
+            Bundle.setDefusable(adjustment.getSignals(), true);
+            n.sbn.setOverrideGroupKey(adjustment.getSignals().getString(
+                    Adjustment.GROUP_KEY_OVERRIDE_KEY, null));
+        }
+    }
+
+    // Clears the 'fake' auto-bunding summary.
+    private void maybeClearAutobundleSummaryLocked(Adjustment adjustment) {
+        if (adjustment.getSignals() != null
+                && adjustment.getSignals().containsKey(Adjustment.NEEDS_AUTOGROUPING_KEY)
+                && !adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) {
+            if (mAutobundledSummaries.containsKey(adjustment.getPackage())) {
+                // Clear summary.
+                final NotificationRecord removed = mNotificationsByKey.get(
+                        mAutobundledSummaries.remove(adjustment.getPackage()));
+                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
+                && 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) {
+                if (!mAutobundledSummaries.containsKey(adjustment.getPackage())
+                        && newAutoBundleKey != null) {
+                    // Add summary
+                    final StatusBarNotification adjustedSbn
+                            = mNotificationsByKey.get(adjustment.getKey()).sbn;
+
+                    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)
+                                    .build();
+                    summaryNotification.extras.putAll(extras);
+                    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);
+                    mAutobundledSummaries.put(adjustment.getPackage(), summarySbn.getKey());
+                    userId = adjustedSbn.getUser().getIdentifier();
+                }
+            }
+            if (summaryRecord != null) {
+                mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
+            }
+        }
+    }
+
     private String disableNotificationEffects(NotificationRecord record) {
         if (mDisableNotificationEffects) {
             return "booleanState";
@@ -2253,6 +2351,17 @@
                 callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
         final UserHandle user = new UserHandle(userId);
 
+        // Fix the notification as best we can.
+        try {
+            Notification.addFieldsFromContext(getContext().createApplicationContext(
+                    getContext().getPackageManager().getApplicationInfoAsUser(
+                            pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId),
+                    Context.CONTEXT_RESTRICTED), notification);
+        } catch (NameNotFoundException e) {
+            Slog.e(TAG, "Cannot create a context for sending app", e);
+            return;
+        }
+
         // Limit the number of notifications that any given package except the android
         // package or a registered listener can enqueue.  Prevents DOS attacks and deals with leaks.
         if (!isSystemNotification && !isNotificationFromListener) {
@@ -2492,7 +2601,7 @@
             StatusBarNotification sbn = r.sbn;
             String group = sbn.getGroupKey();
             boolean isSummary = sbn.getNotification().isGroupSummary();
-            boolean isChild = sbn.getNotification().isGroupChild();
+            boolean isChild = !isSummary && sbn.isGroup();
 
             NotificationRecord summary = mSummaryByGroupKey.get(group);
             if (isChild && summary != null) {
@@ -2857,11 +2966,13 @@
         synchronized (mNotificationList) {
             final int N = mNotificationList.size();
             ArrayList<String> orderBefore = new ArrayList<String>(N);
+            ArrayList<String> groupOverrideBefore = new ArrayList<>(N);
             int[] visibilities = new int[N];
-            int [] importances = new int[N];
+            int[] importances = new int[N];
             for (int i = 0; i < N; i++) {
                 final NotificationRecord r = mNotificationList.get(i);
                 orderBefore.add(r.getKey());
+                groupOverrideBefore.add(r.sbn.getGroupKey());
                 visibilities[i] = r.getPackageVisibilityOverride();
                 importances[i] = r.getImportance();
                 mRankingHelper.extractSignals(r);
@@ -2871,7 +2982,8 @@
                 final NotificationRecord r = mNotificationList.get(i);
                 if (!orderBefore.get(i).equals(r.getKey())
                         || visibilities[i] != r.getPackageVisibilityOverride()
-                        || importances[i] != r.getImportance()) {
+                        || importances[i] != r.getImportance()
+                        || !groupOverrideBefore.get(i).equals(r.sbn.getGroupKey())) {
                     scheduleSendRankingUpdate();
                     return;
                 }
@@ -3070,6 +3182,7 @@
         mLights.remove(canceledKey);
 
         // Record usage stats
+        // TODO: add unbundling stats?
         switch (reason) {
             case REASON_DELEGATE_CANCEL:
             case REASON_DELEGATE_CANCEL_ALL:
@@ -3089,6 +3202,9 @@
         if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) {
             mSummaryByGroupKey.remove(groupKey);
         }
+        if (r.sbn.getKey().equals(mAutobundledSummaries.get(r.sbn.getPackageName()))) {
+            mAutobundledSummaries.remove(r.sbn.getPackageName());
+        }
 
         // Save it for users of getHistoricalNotifications()
         mArchive.record(r.sbn);
@@ -3287,7 +3403,7 @@
         for (int i = N - 1; i >= 0; i--) {
             NotificationRecord childR = mNotificationList.get(i);
             StatusBarNotification childSbn = childR.sbn;
-            if (childR.getNotification().isGroupChild() &&
+            if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) &&
                     childR.getGroupKey().equals(r.getGroupKey())) {
                 EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(),
                         childSbn.getTag(), userId, 0, 0, reason, listenerName);
@@ -3438,6 +3554,7 @@
         ArrayList<String> keys = new ArrayList<String>(N);
         ArrayList<String> interceptedKeys = new ArrayList<String>(N);
         ArrayList<Integer> importance = new ArrayList<>(N);
+        Bundle overrideGroupKeys = new Bundle();
         Bundle visibilityOverrides = new Bundle();
         Bundle suppressedVisualEffects = new Bundle();
         Bundle explanation = new Bundle();
@@ -3461,6 +3578,7 @@
                     != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) {
                 visibilityOverrides.putInt(key, record.getPackageVisibilityOverride());
             }
+            overrideGroupKeys.putString(key, record.sbn.getOverrideGroupKey());
         }
         final int M = keys.size();
         String[] keysAr = keys.toArray(new String[M]);
@@ -3470,7 +3588,7 @@
             importanceAr[i] = importance.get(i);
         }
         return new NotificationRankingUpdate(keysAr, interceptedKeysAr, visibilityOverrides,
-                suppressedVisualEffects, importanceAr, explanation);
+                suppressedVisualEffects, importanceAr, explanation, overrideGroupKeys);
     }
 
     private boolean isVisibleToListener(StatusBarNotification sbn, ManagedServiceInfo listener) {
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index fd893fa..a89a422 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -24,18 +24,15 @@
 
 import android.app.Notification;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
-import android.os.Build;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
-import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.EventLogTags;
diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
index 0da1bb1..6c8be39 100644
--- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
+++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
@@ -86,6 +86,155 @@
     }
 
     private Test[] mTests = new Test[] {
+            new Test("Post a group") {
+                public void run()
+                {
+                    Notification n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("Min priority group 1")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setGroup("group1")
+                            .build();
+                    mNM.notify(6000, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("low priority group 1")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_LOW)
+                            .setGroup("group1")
+                            .build();
+                    mNM.notify(6001, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("default priority group 1")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setGroup("group1")
+                            .build();
+                    mNM.notify(6002, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("summary group 1")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setGroup("group1")
+                            .setGroupSummary(true)
+                            .build();
+                    mNM.notify(6003, n);
+                }
+            },
+            new Test("Post a group (2) w/o summary") {
+                public void run()
+                {
+                    Notification n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("Min priority group 2")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setGroup("group2")
+                            .build();
+                    mNM.notify(6100, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("low priority group 2")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_LOW)
+                            .setGroup("group2")
+                            .build();
+                    mNM.notify(6101, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("default priority group 2")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setGroup("group2")
+                            .build();
+                    mNM.notify(6102, n);
+                }
+            },
+            new Test("Summary for group 2") {
+                public void run()
+                {
+                    Notification n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("summary group 2")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setGroup("group2")
+                            .setGroupSummary(true)
+                            .build();
+                    mNM.notify(6103, n);
+                }
+            },
+            new Test("Group up public-secret") {
+                public void run()
+                {
+                    Notification n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("public notification")
+                            .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setVisibility(Notification.VISIBILITY_PUBLIC)
+                            .setGroup("public-secret")
+                            .build();
+                    mNM.notify("public", 7009, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("private only notification")
+                            .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setVisibility(Notification.VISIBILITY_PRIVATE)
+                            .setGroup("public-secret")
+                            .build();
+                    mNM.notify("no public", 7010, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("private version of notification")
+                            .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setVisibility(Notification.VISIBILITY_PRIVATE)
+                            .setGroup("public-secret")
+                            .setPublicVersion(new Notification.Builder(NotificationTestList.this)
+                                    .setSmallIcon(R.drawable.icon2)
+                                    .setContentTitle("public notification of private notification")
+                                    .setPriority(Notification.PRIORITY_DEFAULT)
+                                    .setVisibility(Notification.VISIBILITY_PUBLIC)
+                                    .build())
+                            .build();
+                    mNM.notify("priv with pub", 7011, n);
+                    n = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("secret notification")
+                            .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE)
+                            .setPriority(Notification.PRIORITY_DEFAULT)
+                            .setVisibility(Notification.VISIBILITY_SECRET)
+                            .setGroup("public-secret")
+                            .build();
+                    mNM.notify("secret", 7012, n);
+
+                    Notification s = new Notification.Builder(NotificationTestList.this)
+                            .setSmallIcon(R.drawable.icon2)
+                            .setContentTitle("summary group public-secret")
+                            .setLights(0xff0000ff, 1, 0)
+                            .setPriority(Notification.PRIORITY_MIN)
+                            .setGroup("public-secret")
+                            .setGroupSummary(true)
+                            .build();
+                    mNM.notify(7113, s);
+                }
+            },
+            new Test("Cancel priority autogroup") {
+                public void run()
+                {
+                    try {
+                        mNM.cancel(Integer.MAX_VALUE);
+                    } catch (Exception e) {
+                        Toast.makeText(NotificationTestList.this, "cancel failed (yay)",
+                                Toast.LENGTH_LONG).show();
+                    }
+                }
+            },
             new Test("Min priority") {
                 public void run()
                 {
@@ -95,7 +244,7 @@
                             .setLights(0xff0000ff, 1, 0)
                             .setPriority(Notification.PRIORITY_MIN)
                             .build();
-                    mNM.notify(7000, n);
+                    mNM.notify("min", 7000, n);
                 }
             },
             new Test("Min priority, high pri flag") {
@@ -123,7 +272,7 @@
                             .setLights(0xff0000ff, 1, 0)
                             .setPriority(Notification.PRIORITY_LOW)
                             .build();
-                    mNM.notify(7002, n);
+                    mNM.notify("low", 7002, n);
                 }
             },
             new Test("Default priority") {
@@ -135,7 +284,7 @@
                             .setLights(0xff0000ff, 1, 0)
                             .setPriority(Notification.PRIORITY_DEFAULT)
                             .build();
-                    mNM.notify(7004, n);
+                    mNM.notify("default", 7004, n);
                 }
             },
             new Test("High priority") {
@@ -150,7 +299,7 @@
                                     getPackageName() + "/raw/ringer"))
                             .setPriority(Notification.PRIORITY_HIGH)
                             .build();
-                    mNM.notify(7006, n);
+                    mNM.notify("high", 7006, n);
                 }
             },
             new Test("Max priority") {
@@ -166,7 +315,7 @@
                             .setPriority(Notification.PRIORITY_MAX)
                             .setFullScreenIntent(makeIntent2(), false)
                             .build();
-                    mNM.notify(7007, n);
+                    mNM.notify("max", 7007, n);
                 }
             },
             new Test("Max priority with delay") {
@@ -199,7 +348,7 @@
                             .setPriority(Notification.PRIORITY_DEFAULT)
                             .setVisibility(Notification.VISIBILITY_PUBLIC)
                             .build();
-                    mNM.notify(7009, n);
+                    mNM.notify("public", 7009, n);
                 }
             },
             new Test("private notification, no public") {
@@ -212,7 +361,7 @@
                             .setPriority(Notification.PRIORITY_DEFAULT)
                             .setVisibility(Notification.VISIBILITY_PRIVATE)
                             .build();
-                    mNM.notify(7010, n);
+                    mNM.notify("no public", 7010, n);
                 }
             },
             new Test("private notification, has public") {
@@ -231,7 +380,7 @@
                                     .setVisibility(Notification.VISIBILITY_PUBLIC)
                                     .build())
                             .build();
-                    mNM.notify(7011, n);
+                    mNM.notify("priv with pub", 7011, n);
                 }
             },
             new Test("secret notification") {
@@ -244,7 +393,7 @@
                             .setPriority(Notification.PRIORITY_DEFAULT)
                             .setVisibility(Notification.VISIBILITY_SECRET)
                             .build();
-                    mNM.notify(7012, n);
+                    mNM.notify("secret", 7012, n);
                 }
             },
         new Test("Off") {