Add more functionality to channel groups

Description, blocking, links to/from the app

Test: cts, runtest systemui-notification
Bug: 63927402
Change-Id: Icc8caf319651f9ac2d622fb54110270c89bdff61
diff --git a/api/current.txt b/api/current.txt
index d94614b..53e6a94 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5161,6 +5161,7 @@
     field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
     field public static final java.lang.String EXTRA_COLORIZED = "android.colorized";
@@ -5576,8 +5577,11 @@
     method public android.app.NotificationChannelGroup clone();
     method public int describeContents();
     method public java.util.List<android.app.NotificationChannel> getChannels();
+    method public java.lang.String getDescription();
     method public java.lang.String getId();
     method public java.lang.CharSequence getName();
+    method public boolean isBlocked();
+    method public void setDescription(java.lang.String);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.app.NotificationChannelGroup> CREATOR;
   }
@@ -34883,6 +34887,7 @@
     field public static final java.lang.String ACTION_BLUETOOTH_SETTINGS = "android.settings.BLUETOOTH_SETTINGS";
     field public static final java.lang.String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS";
     field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
+    field public static final java.lang.String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_CHANNEL_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
     field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
@@ -34942,6 +34947,7 @@
     field public static final java.lang.String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE";
     field public static final java.lang.String EXTRA_AUTHORITIES = "authorities";
     field public static final java.lang.String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_ENABLED = "android.settings.extra.do_not_disturb_mode_enabled";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_MINUTES = "android.settings.extra.do_not_disturb_mode_minutes";
diff --git a/api/system-current.txt b/api/system-current.txt
index a43fe2e..26f923d 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5346,6 +5346,7 @@
     field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
     field public static final java.lang.String EXTRA_COLORIZED = "android.colorized";
@@ -5782,8 +5783,11 @@
     method public android.app.NotificationChannelGroup clone();
     method public int describeContents();
     method public java.util.List<android.app.NotificationChannel> getChannels();
+    method public java.lang.String getDescription();
     method public java.lang.String getId();
     method public java.lang.CharSequence getName();
+    method public boolean isBlocked();
+    method public void setDescription(java.lang.String);
     method public org.json.JSONObject toJson() throws org.json.JSONException;
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.app.NotificationChannelGroup> CREATOR;
@@ -37935,6 +37939,7 @@
     field public static final java.lang.String ACTION_BLUETOOTH_SETTINGS = "android.settings.BLUETOOTH_SETTINGS";
     field public static final java.lang.String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS";
     field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
+    field public static final java.lang.String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_CHANNEL_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
     field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
@@ -37995,6 +38000,7 @@
     field public static final java.lang.String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE";
     field public static final java.lang.String EXTRA_AUTHORITIES = "authorities";
     field public static final java.lang.String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_ENABLED = "android.settings.extra.do_not_disturb_mode_enabled";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_MINUTES = "android.settings.extra.do_not_disturb_mode_minutes";
diff --git a/api/test-current.txt b/api/test-current.txt
index 87eb171..e0989e8 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5174,6 +5174,7 @@
     field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
     field public static final java.lang.String EXTRA_COLORIZED = "android.colorized";
@@ -5589,8 +5590,12 @@
     method public android.app.NotificationChannelGroup clone();
     method public int describeContents();
     method public java.util.List<android.app.NotificationChannel> getChannels();
+    method public java.lang.String getDescription();
     method public java.lang.String getId();
     method public java.lang.CharSequence getName();
+    method public boolean isBlocked();
+    method public void setBlocked(boolean);
+    method public void setDescription(java.lang.String);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.app.NotificationChannelGroup> CREATOR;
   }
@@ -35060,6 +35065,7 @@
     field public static final java.lang.String ACTION_BLUETOOTH_SETTINGS = "android.settings.BLUETOOTH_SETTINGS";
     field public static final java.lang.String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS";
     field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
+    field public static final java.lang.String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_CHANNEL_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
     field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
     field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
@@ -35120,6 +35126,7 @@
     field public static final java.lang.String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE";
     field public static final java.lang.String EXTRA_AUTHORITIES = "authorities";
     field public static final java.lang.String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled";
+    field public static final java.lang.String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_ENABLED = "android.settings.extra.do_not_disturb_mode_enabled";
     field public static final java.lang.String EXTRA_DO_NOT_DISTURB_MODE_MINUTES = "android.settings.extra.do_not_disturb_mode_minutes";
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 08821be..d4752a7 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -61,6 +61,8 @@
     void createNotificationChannelsForPackage(String pkg, int uid, in ParceledListSlice channelsList);
     ParceledListSlice getNotificationChannelGroupsForPackage(String pkg, int uid, boolean includeDeleted);
     NotificationChannelGroup getNotificationChannelGroupForPackage(String groupId, String pkg, int uid);
+    NotificationChannelGroup getPopulatedNotificationChannelGroupForPackage(String pkg, int uid, String groupId, boolean includeDeleted);
+    void updateNotificationChannelGroupForPackage(String pkg, int uid, in NotificationChannelGroup group);
     void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel);
     NotificationChannel getNotificationChannel(String pkg, String channelId);
     NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted);
@@ -103,6 +105,7 @@
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
     void setInterruptionFilter(String pkg, int interruptionFilter);
 
+    void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group);
     void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel);
     ParceledListSlice getNotificationChannelsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user);
     ParceledListSlice getNotificationChannelGroupsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 9511f3f..841b961 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -124,6 +124,13 @@
 
     /**
      * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
+     * contain a {@link NotificationChannelGroup#getId() group id} that can be used to narrow down
+     * what settings should be shown in the target app.
+     */
+    public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
+
+    /**
+     * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
      * contain the tag provided to {@link NotificationManager#notify(String, int, Notification)}
      * that can be used to narrow down what settings should be shown in the target app.
      */
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index d6e3691..163a8dc 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -830,24 +830,24 @@
 
     @Override
     public String toString() {
-        return "NotificationChannel{" +
-                "mId='" + mId + '\'' +
-                ", mName=" + mName +
-                ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") +
-                ", mImportance=" + mImportance +
-                ", mBypassDnd=" + mBypassDnd +
-                ", mLockscreenVisibility=" + mLockscreenVisibility +
-                ", mSound=" + mSound +
-                ", mLights=" + mLights +
-                ", mLightColor=" + mLightColor +
-                ", mVibration=" + Arrays.toString(mVibration) +
-                ", mUserLockedFields=" + mUserLockedFields +
-                ", mVibrationEnabled=" + mVibrationEnabled +
-                ", mShowBadge=" + mShowBadge +
-                ", mDeleted=" + mDeleted +
-                ", mGroup='" + mGroup + '\'' +
-                ", mAudioAttributes=" + mAudioAttributes +
-                ", mBlockableSystem=" + mBlockableSystem +
-                '}';
+        return "NotificationChannel{"
+                + "mId='" + mId + '\''
+                + ", mName=" + mName
+                + ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "")
+                + ", mImportance=" + mImportance
+                + ", mBypassDnd=" + mBypassDnd
+                + ", mLockscreenVisibility=" + mLockscreenVisibility
+                + ", mSound=" + mSound
+                + ", mLights=" + mLights
+                + ", mLightColor=" + mLightColor
+                + ", mVibration=" + Arrays.toString(mVibration)
+                + ", mUserLockedFields=" + Integer.toHexString(mUserLockedFields)
+                + ", mVibrationEnabled=" + mVibrationEnabled
+                + ", mShowBadge=" + mShowBadge
+                + ", mDeleted=" + mDeleted
+                + ", mGroup='" + mGroup + '\''
+                + ", mAudioAttributes=" + mAudioAttributes
+                + ", mBlockableSystem=" + mBlockableSystem
+                + '}';
     }
 }
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 18ad9cf..5173311 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -16,6 +16,7 @@
 package android.app;
 
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.content.Intent;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -23,6 +24,7 @@
 
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.IOException;
@@ -42,10 +44,14 @@
 
     private static final String TAG_GROUP = "channelGroup";
     private static final String ATT_NAME = "name";
+    private static final String ATT_DESC = "desc";
     private static final String ATT_ID = "id";
+    private static final String ATT_BLOCKED = "blocked";
 
     private final String mId;
     private CharSequence mName;
+    private String mDescription;
+    private boolean mBlocked;
     private List<NotificationChannel> mChannels = new ArrayList<>();
 
     /**
@@ -73,7 +79,13 @@
             mId = null;
         }
         mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        if (in.readByte() != 0) {
+            mDescription = in.readString();
+        } else {
+            mDescription = null;
+        }
         in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader());
+        mBlocked = in.readBoolean();
     }
 
     private String getTrimmedString(String input) {
@@ -92,24 +104,38 @@
             dest.writeByte((byte) 0);
         }
         TextUtils.writeToParcel(mName, dest, flags);
+        if (mDescription != null) {
+            dest.writeByte((byte) 1);
+            dest.writeString(mDescription);
+        } else {
+            dest.writeByte((byte) 0);
+        }
         dest.writeParcelableList(mChannels, flags);
+        dest.writeBoolean(mBlocked);
     }
 
     /**
-     * Returns the id of this channel.
+     * Returns the id of this group.
      */
     public String getId() {
         return mId;
     }
 
     /**
-     * Returns the user visible name of this channel.
+     * Returns the user visible name of this group.
      */
     public CharSequence getName() {
         return mName;
     }
 
     /**
+     * Returns the user visible description of this group.
+     */
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
      * Returns the list of channels that belong to this group
      */
     public List<NotificationChannel> getChannels() {
@@ -117,6 +143,32 @@
     }
 
     /**
+     * Returns whether or not notifications posted to {@link NotificationChannel channels} belonging
+     * to this group are blocked.
+     */
+    public boolean isBlocked() {
+        return mBlocked;
+    }
+
+    /**
+     * Sets the user visible description of this group.
+     *
+     * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too
+     * long.
+     */
+    public void setDescription(String description) {
+        mDescription = getTrimmedString(description);
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public void setBlocked(boolean blocked) {
+        mBlocked = blocked;
+    }
+
+    /**
      * @hide
      */
     public void addChannel(NotificationChannel channel) {
@@ -126,6 +178,28 @@
     /**
      * @hide
      */
+    public void setChannels(List<NotificationChannel> channels) {
+        mChannels = channels;
+    }
+
+    /**
+     * @hide
+     */
+    public void populateFromXml(XmlPullParser parser) {
+        // Name, id, and importance are set in the constructor.
+        setDescription(parser.getAttributeValue(null, ATT_DESC));
+        setBlocked(safeBool(parser, ATT_BLOCKED, false));
+    }
+
+    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
+        final String value = parser.getAttributeValue(null, att);
+        if (TextUtils.isEmpty(value)) return defValue;
+        return Boolean.parseBoolean(value);
+    }
+
+    /**
+     * @hide
+     */
     public void writeXml(XmlSerializer out) throws IOException {
         out.startTag(null, TAG_GROUP);
 
@@ -133,6 +207,10 @@
         if (getName() != null) {
             out.attribute(null, ATT_NAME, getName().toString());
         }
+        if (getDescription() != null) {
+            out.attribute(null, ATT_DESC, getDescription().toString());
+        }
+        out.attribute(null, ATT_BLOCKED, Boolean.toString(isBlocked()));
 
         out.endTag(null, TAG_GROUP);
     }
@@ -145,6 +223,8 @@
         JSONObject record = new JSONObject();
         record.put(ATT_ID, getId());
         record.put(ATT_NAME, getName());
+        record.put(ATT_DESC, getDescription());
+        record.put(ATT_BLOCKED, isBlocked());
         return record;
     }
 
@@ -173,31 +253,46 @@
 
         NotificationChannelGroup that = (NotificationChannelGroup) o;
 
+        if (isBlocked() != that.isBlocked()) return false;
         if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
         if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
             return false;
         }
-        return true;
-    }
-
-    @Override
-    public NotificationChannelGroup clone() {
-        return new NotificationChannelGroup(getId(), getName());
+        if (getDescription() != null ? !getDescription().equals(that.getDescription())
+                : that.getDescription() != null) {
+            return false;
+        }
+        return getChannels() != null ? getChannels().equals(that.getChannels())
+                : that.getChannels() == null;
     }
 
     @Override
     public int hashCode() {
         int result = getId() != null ? getId().hashCode() : 0;
         result = 31 * result + (getName() != null ? getName().hashCode() : 0);
+        result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
+        result = 31 * result + (isBlocked() ? 1 : 0);
+        result = 31 * result + (getChannels() != null ? getChannels().hashCode() : 0);
         return result;
     }
 
     @Override
+    public NotificationChannelGroup clone() {
+        NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName());
+        cloned.setDescription(getDescription());
+        cloned.setBlocked(isBlocked());
+        cloned.setChannels(getChannels());
+        return cloned;
+    }
+
+    @Override
     public String toString() {
-        return "NotificationChannelGroup{" +
-                "mId='" + mId + '\'' +
-                ", mName=" + mName +
-                ", mChannels=" + mChannels +
-                '}';
+        return "NotificationChannelGroup{"
+                + "mId='" + mId + '\''
+                + ", mName=" + mName
+                + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "")
+                + ", mBlocked=" + mBlocked
+                + ", mChannels=" + mChannels
+                + '}';
     }
 }
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 34343e9..8fa7d6c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -421,7 +421,7 @@
      * Creates a notification channel that notifications can be posted to.
      *
      * This can also be used to restore a deleted channel and to update an existing channel's
-     * name, description, and/or importance.
+     * name, description, group, and/or importance.
      *
      * <p>The name and description should only be changed if the locale changes
      * or in response to the user renaming this channel. For example, if a user has a channel
@@ -431,6 +431,9 @@
      * <p>The importance of an existing channel will only be changed if the new importance is lower
      * than the current value and the user has not altered any settings on this channel.
      *
+     * <p>The group an existing channel will only be changed if the channel does not already
+     * belong to a group.
+     *
      * All other fields are ignored for channels that already exist.
      *
      * @param channel  the channel to create.  Note that the created channel may differ from this
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0ff4adc..872ac63 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -31,6 +31,7 @@
 import android.app.AppOpsManager;
 import android.app.Application;
 import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
 import android.app.SearchManager;
 import android.app.WallpaperManager;
@@ -1311,6 +1312,18 @@
             = "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
 
     /**
+     * Activity Action: Show notification settings for a single {@link NotificationChannelGroup}.
+     * <p>
+     *     Input: {@link #EXTRA_APP_PACKAGE}, the package containing the channel group to display.
+     *     Input: {@link #EXTRA_CHANNEL_GROUP_ID}, the id of the channel group to display.
+     * <p>
+     * Output: Nothing.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS =
+            "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
+
+    /**
      * Activity Extra: The package owner of the notification channel settings to display.
      * <p>
      * This must be passed as an extra field to the {@link #ACTION_CHANNEL_NOTIFICATION_SETTINGS}.
@@ -1326,6 +1339,15 @@
     public static final String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
 
     /**
+     * Activity Extra: The {@link NotificationChannelGroup#getId()} of the notification channel
+     * group settings to display.
+     * <p>
+     * This must be passed as an extra field to the
+     * {@link #ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS}.
+     */
+    public static final String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
+
+    /**
      * Activity Action: Show notification redaction settings.
      *
      * @hide
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 3bc5923..a79f244 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -1516,7 +1516,7 @@
     // OS: N
     ACTION_ZEN_ALLOW_LIGHTS = 264;
 
-    // OPEN: Settings > Notifications > [App] > Topic Notifications
+    // OPEN: Settings > Notifications > [App] > Channel Notifications
     // CATEGORY: SETTINGS
     // OS: N
     NOTIFICATION_TOPIC_NOTIFICATION = 265;
@@ -4354,6 +4354,11 @@
     // CATEGORY: SETTINGS
     SETTINGS_FEATURE_FLAGS_DASHBOARD = 1156;
 
+    // OPEN: Settings > Notifications > [App] > Topic Notifications
+    // CATEGORY: SETTINGS
+    // OS: P
+    NOTIFICATION_CHANNEL_GROUP = 1157;
+
     // Add new aosp constants above this line.
     // END OF AOSP CONSTANTS
   }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index a693e97..7dbacdc 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1470,6 +1470,19 @@
         savePolicyFile();
     }
 
+    private void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
+            boolean fromApp, boolean fromListener) {
+        Preconditions.checkNotNull(group);
+        Preconditions.checkNotNull(pkg);
+        mRankingHelper.createNotificationChannelGroup(pkg, uid, group,
+                fromApp);
+        if (!fromListener) {
+            mListeners.notifyNotificationChannelGroupChanged(pkg,
+                    UserHandle.of(UserHandle.getCallingUserId()), group,
+                    NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
+        }
+    }
+
     private ArrayList<ComponentName> getSuppressors() {
         ArrayList<ComponentName> names = new ArrayList<ComponentName>();
         for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) {
@@ -1757,6 +1770,14 @@
         }
 
         @Override
+        public void updateNotificationChannelGroupForPackage(String pkg, int uid,
+                NotificationChannelGroup group) throws RemoteException {
+            enforceSystemOrSystemUI("Caller not system or systemui");
+            createNotificationChannelGroup(pkg, uid, group, false, false);
+            savePolicyFile();
+        }
+
+        @Override
         public void createNotificationChannelGroups(String pkg,
                 ParceledListSlice channelGroupList) throws RemoteException {
             checkCallerIsSystemOrSameApp(pkg);
@@ -1764,12 +1785,7 @@
             final int groupSize = groups.size();
             for (int i = 0; i < groupSize; i++) {
                 final NotificationChannelGroup group = groups.get(i);
-                Preconditions.checkNotNull(group, "group in list is null");
-                mRankingHelper.createNotificationChannelGroup(pkg, Binder.getCallingUid(), group,
-                        true /* fromTargetApp */);
-                mListeners.notifyNotificationChannelGroupChanged(pkg,
-                        UserHandle.of(UserHandle.getCallingUserId()), group,
-                        NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
+                createNotificationChannelGroup(pkg, Binder.getCallingUid(), group, true, false);
             }
             savePolicyFile();
         }
@@ -1915,6 +1931,14 @@
         }
 
         @Override
+        public NotificationChannelGroup getPopulatedNotificationChannelGroupForPackage(
+                String pkg, int uid, String groupId, boolean includeDeleted) {
+            enforceSystemOrSystemUI("getPopulatedNotificationChannelGroupForPackage");
+            return mRankingHelper.getNotificationChannelGroupWithChannels(
+                    pkg, uid, groupId, includeDeleted);
+        }
+
+        @Override
         public NotificationChannelGroup getNotificationChannelGroupForPackage(
                 String groupId, String pkg, int uid) {
             enforceSystemOrSystemUI("getNotificationChannelGroupForPackage");
@@ -2917,6 +2941,17 @@
         }
 
         @Override
+        public void updateNotificationChannelGroupFromPrivilegedListener(
+                INotificationListener token, String pkg, UserHandle user,
+                NotificationChannelGroup group) throws RemoteException {
+            Preconditions.checkNotNull(user);
+            verifyPrivilegedListener(token, user);
+            createNotificationChannelGroup(
+                    pkg, getUidForPackageAndUser(pkg, user), group, false, true);
+            savePolicyFile();
+        }
+
+        @Override
         public void updateNotificationChannelFromPrivilegedListener(INotificationListener token,
                 String pkg, UserHandle user, NotificationChannel channel) throws RemoteException {
             Preconditions.checkNotNull(channel);
@@ -3612,9 +3647,10 @@
             usageStats.registerSuspendedByAdmin(r);
             return isPackageSuspended;
         }
-
         final boolean isBlocked =
-                mRankingHelper.getImportance(pkg, callingUid) == NotificationManager.IMPORTANCE_NONE
+                mRankingHelper.isGroupBlocked(pkg, callingUid, r.getChannel().getGroup())
+                || mRankingHelper.getImportance(pkg, callingUid)
+                        == NotificationManager.IMPORTANCE_NONE
                 || r.getChannel().getImportance() == NotificationManager.IMPORTANCE_NONE;
         if (isBlocked) {
             Slog.e(TAG, "Suppressing notification from package by user request.");
diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java
index 36da04d..b5ef1c6 100644
--- a/services/core/java/com/android/server/notification/RankingConfig.java
+++ b/services/core/java/com/android/server/notification/RankingConfig.java
@@ -29,6 +29,7 @@
     void setShowBadge(String packageName, int uid, boolean showBadge);
     boolean canShowBadge(String packageName, int uid);
     boolean badgingEnabled(UserHandle userHandle);
+    boolean isGroupBlocked(String packageName, int uid, String groupId);
 
     Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid);
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 3386fe83..1736a74 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -215,6 +215,7 @@
                                 if (!TextUtils.isEmpty(id)) {
                                     NotificationChannelGroup group
                                             = new NotificationChannelGroup(id, groupName);
+                                    group.populateFromXml(parser);
                                     r.groups.put(id, group);
                                 }
                             }
@@ -493,6 +494,19 @@
         updateConfig();
     }
 
+    @Override
+    public boolean isGroupBlocked(String packageName, int uid, String groupId) {
+        if (groupId == null) {
+            return false;
+        }
+        Record r = getOrCreateRecord(packageName, uid);
+        NotificationChannelGroup group = r.groups.get(groupId);
+        if (group == null) {
+            return false;
+        }
+        return group.isBlocked();
+    }
+
     int getPackagePriority(String pkg, int uid) {
         return getOrCreateRecord(pkg, uid).priority;
     }
@@ -514,9 +528,16 @@
         }
         final NotificationChannelGroup oldGroup = r.groups.get(group.getId());
         if (!group.equals(oldGroup)) {
-            // will log for new entries as well as name changes
+            // will log for new entries as well as name/description changes
             MetricsLogger.action(getChannelGroupLog(group.getId(), pkg));
         }
+        if (oldGroup != null) {
+            group.setChannels(oldGroup.getChannels());
+
+            if (fromTargetApp) {
+                group.setBlocked(oldGroup.isBlocked());
+            }
+        }
         r.groups.put(group.getId(), group);
     }
 
@@ -552,6 +573,9 @@
             existing.setName(channel.getName().toString());
             existing.setDescription(channel.getDescription());
             existing.setBlockableSystem(channel.isBlockableSystem());
+            if (existing.getGroup() == null) {
+                existing.setGroup(channel.getGroup());
+            }
 
             // Apps are allowed to downgrade channel importance if the user has not changed any
             // fields on this channel yet.
@@ -684,6 +708,27 @@
         }
     }
 
+    public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
+            int uid, String groupId, boolean includeDeleted) {
+        Preconditions.checkNotNull(pkg);
+        Record r = getRecord(pkg, uid);
+        if (r == null || groupId == null || !r.groups.containsKey(groupId)) {
+            return null;
+        }
+        NotificationChannelGroup group = r.groups.get(groupId).clone();
+        group.setChannels(new ArrayList<>());
+        int N = r.channels.size();
+        for (int i = 0; i < N; i++) {
+            final NotificationChannel nc = r.channels.valueAt(i);
+            if (includeDeleted || !nc.isDeleted()) {
+                if (groupId.equals(nc.getGroup())) {
+                    group.addChannel(nc);
+                }
+            }
+        }
+        return group;
+    }
+
     public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
             int uid) {
         Preconditions.checkNotNull(pkg);
@@ -710,6 +755,7 @@
                         NotificationChannelGroup ncg = groups.get(nc.getGroup());
                         if (ncg == null) {
                             ncg = r.groups.get(nc.getGroup()).clone();
+                            ncg.setChannels(new ArrayList<>());
                             groups.put(nc.getGroup(), ncg);
 
                         }
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
index 04b42f1..ddd21df 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -429,6 +429,20 @@
     }
 
     @Test
+    public void testBlockedNotifications_blockedChannelGroup() throws Exception {
+        when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+        mNotificationManagerService.setRankingHelper(mRankingHelper);
+        when(mRankingHelper.isGroupBlocked(anyString(), anyInt(), anyString())).thenReturn(true);
+
+        NotificationChannel channel = new NotificationChannel("id", "name",
+                NotificationManager.IMPORTANCE_HIGH);
+        channel.setGroup("something");
+        NotificationRecord r = generateNotificationRecord(channel);
+        assertTrue(mNotificationManagerService.isBlocked(r, mUsageStats));
+        verify(mUsageStats, times(1)).registerBlocked(eq(r));
+    }
+
+    @Test
     public void testEnqueuedBlockedNotifications_blockedApp() throws Exception {
         when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
 
diff --git a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
index 65bf330..306dd98 100644
--- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
@@ -240,12 +240,23 @@
     private void compareGroups(NotificationChannelGroup expected, NotificationChannelGroup actual) {
         assertEquals(expected.getId(), actual.getId());
         assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.isBlocked(), actual.isBlocked());
     }
 
     private NotificationChannel getChannel() {
         return new NotificationChannel("id", "name", IMPORTANCE_LOW);
     }
 
+    private NotificationChannel findChannel(List<NotificationChannel> channels, String id) {
+        for (NotificationChannel channel : channels) {
+            if (channel.getId().equals(id)) {
+                return channel;
+            }
+        }
+        return null;
+    }
+
     @Test
     public void testFindAfterRankingWithASplitGroup() throws Exception {
         ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(3);
@@ -299,6 +310,8 @@
     @Test
     public void testChannelXml() throws Exception {
         NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye");
+        ncg.setBlocked(true);
+        ncg.setDescription("group desc");
         NotificationChannelGroup ncg2 = new NotificationChannelGroup("2", "hello");
         NotificationChannel channel1 =
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
@@ -1294,6 +1307,26 @@
     }
 
     @Test
+    public void testCreateChannel_addToGroup() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("group", "");
+        mHelper.createNotificationChannelGroup(PKG, UID, group, true);
+        NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG, UID, nc, true);
+        NotificationChannel actual = mHelper.getNotificationChannel(PKG, UID, "id", false);
+        assertNull(actual.getGroup());
+
+        nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH);
+        nc.setGroup(group.getId());
+        mHelper.createNotificationChannel(PKG, UID, nc, true);
+
+        actual = mHelper.getNotificationChannel(PKG, UID, "id", false);
+        assertNotNull(actual.getGroup());
+        assertEquals(IMPORTANCE_DEFAULT, actual.getImportance());
+
+        verify(mHandler, times(1)).requestSort();
+    }
+
+    @Test
     public void testDumpChannelsJson() throws Exception {
         final ApplicationInfo upgrade = new ApplicationInfo();
         upgrade.targetSdkVersion = Build.VERSION_CODES.O;
@@ -1388,4 +1421,77 @@
         assertEquals(newLabel, mHelper.getNotificationChannel(PKG, UID,
                 NotificationChannel.DEFAULT_CHANNEL_ID, false).getName());
     }
+
+    @Test
+    public void testIsGroupBlocked_noGroup() throws Exception {
+        assertFalse(mHelper.isGroupBlocked(PKG, UID, null));
+
+        assertFalse(mHelper.isGroupBlocked(PKG, UID, "non existent group"));
+    }
+
+    @Test
+    public void testIsGroupBlocked_notBlocked() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("id", "name");
+        mHelper.createNotificationChannelGroup(PKG, UID, group, true);
+
+        assertFalse(mHelper.isGroupBlocked(PKG, UID, group.getId()));
+    }
+
+    @Test
+    public void testIsGroupBlocked_blocked() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("id", "name");
+        mHelper.createNotificationChannelGroup(PKG, UID, group, true);
+        group.setBlocked(true);
+        mHelper.createNotificationChannelGroup(PKG, UID, group, false);
+
+        assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId()));
+    }
+
+    @Test
+    public void testIsGroup_appCannotResetBlock() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("id", "name");
+        mHelper.createNotificationChannelGroup(PKG, UID, group, true);
+        NotificationChannelGroup group2 = group.clone();
+        group2.setBlocked(true);
+        mHelper.createNotificationChannelGroup(PKG, UID, group2, false);
+        assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId()));
+
+        NotificationChannelGroup group3 = group.clone();
+        group3.setBlocked(false);
+        mHelper.createNotificationChannelGroup(PKG, UID, group3, true);
+        assertTrue(mHelper.isGroupBlocked(PKG, UID, group.getId()));
+    }
+
+    @Test
+    public void testGetNotificationChannelGroupWithChannels() throws Exception {
+        NotificationChannelGroup group = new NotificationChannelGroup("group", "");
+        NotificationChannelGroup other = new NotificationChannelGroup("something else", "");
+        mHelper.createNotificationChannelGroup(PKG, UID, group, true);
+        mHelper.createNotificationChannelGroup(PKG, UID, other, true);
+
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT);
+        a.setGroup(group.getId());
+        NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT);
+        b.setGroup(other.getId());
+        NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT);
+        c.setGroup(group.getId());
+        NotificationChannel d = new NotificationChannel("d", "d", IMPORTANCE_DEFAULT);
+
+        mHelper.createNotificationChannel(PKG, UID, a, true);
+        mHelper.createNotificationChannel(PKG, UID, b, true);
+        mHelper.createNotificationChannel(PKG, UID, c, true);
+        mHelper.createNotificationChannel(PKG, UID, d, true);
+        mHelper.deleteNotificationChannel(PKG, UID, c.getId());
+
+        NotificationChannelGroup retrieved = mHelper.getNotificationChannelGroupWithChannels(
+                PKG, UID, group.getId(), true);
+        assertEquals(2, retrieved.getChannels().size());
+        compareChannels(a, findChannel(retrieved.getChannels(), a.getId()));
+        compareChannels(c, findChannel(retrieved.getChannels(), c.getId()));
+
+        retrieved = mHelper.getNotificationChannelGroupWithChannels(
+                PKG, UID, group.getId(), false);
+        assertEquals(1, retrieved.getChannels().size());
+        compareChannels(a, findChannel(retrieved.getChannels(), a.getId()));
+    }
 }