Merge "Allow channels to have a user visible description."
diff --git a/api/current.txt b/api/current.txt
index 006fa51..6ecf213 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5507,6 +5507,7 @@
     method public void enableLights(boolean);
     method public void enableVibration(boolean);
     method public android.media.AudioAttributes getAudioAttributes();
+    method public java.lang.String getDescription();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5516,6 +5517,7 @@
     method public android.net.Uri getSound();
     method public long[] getVibrationPattern();
     method public void setBypassDnd(boolean);
+    method public void setDescription(java.lang.String);
     method public void setGroup(java.lang.String);
     method public void setImportance(int);
     method public void setLightColor(int);
diff --git a/api/system-current.txt b/api/system-current.txt
index caae014..2303433 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5692,6 +5692,7 @@
     method public void enableLights(boolean);
     method public void enableVibration(boolean);
     method public android.media.AudioAttributes getAudioAttributes();
+    method public java.lang.String getDescription();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5704,6 +5705,7 @@
     method public boolean isDeleted();
     method public void populateFromXml(org.xmlpull.v1.XmlPullParser);
     method public void setBypassDnd(boolean);
+    method public void setDescription(java.lang.String);
     method public void setGroup(java.lang.String);
     method public void setImportance(int);
     method public void setLightColor(int);
@@ -5719,16 +5721,6 @@
     method public void writeXml(org.xmlpull.v1.XmlSerializer) throws java.io.IOException;
     field public static final android.os.Parcelable.Creator<android.app.NotificationChannel> CREATOR;
     field public static final java.lang.String DEFAULT_CHANNEL_ID = "miscellaneous";
-    field public static final int[] LOCKABLE_FIELDS;
-    field public static final int USER_LOCKED_ALLOWED = 64; // 0x40
-    field public static final int USER_LOCKED_AUDIO_ATTRIBUTES = 256; // 0x100
-    field public static final int USER_LOCKED_IMPORTANCE = 4; // 0x4
-    field public static final int USER_LOCKED_LIGHTS = 8; // 0x8
-    field public static final int USER_LOCKED_PRIORITY = 1; // 0x1
-    field public static final int USER_LOCKED_SHOW_BADGE = 128; // 0x80
-    field public static final int USER_LOCKED_SOUND = 32; // 0x20
-    field public static final int USER_LOCKED_VIBRATION = 16; // 0x10
-    field public static final int USER_LOCKED_VISIBILITY = 2; // 0x2
   }
 
   public final class NotificationChannelGroup implements android.os.Parcelable {
diff --git a/api/test-current.txt b/api/test-current.txt
index 4931006..80e1002 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5517,6 +5517,7 @@
     method public void enableLights(boolean);
     method public void enableVibration(boolean);
     method public android.media.AudioAttributes getAudioAttributes();
+    method public java.lang.String getDescription();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5526,6 +5527,7 @@
     method public android.net.Uri getSound();
     method public long[] getVibrationPattern();
     method public void setBypassDnd(boolean);
+    method public void setDescription(java.lang.String);
     method public void setGroup(java.lang.String);
     method public void setImportance(int);
     method public void setLightColor(int);
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 29c4520..92216d1 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -20,9 +20,6 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlSerializer;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
 import android.annotation.SystemApi;
 import android.content.Intent;
 import android.media.AudioAttributes;
@@ -46,8 +43,15 @@
      */
     public static final String DEFAULT_CHANNEL_ID = "miscellaneous";
 
+    /**
+     * The maximum length for text fields in a NotificationChannel. Fields will be truncated at this
+     * limit.
+     */
+    private static final int MAX_TEXT_LENGTH = 1000;
+
     private static final String TAG_CHANNEL = "channel";
     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_DELETED = "deleted";
     private static final String ATT_PRIORITY = "priority";
@@ -69,56 +73,46 @@
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_PRIORITY = 0x00000001;
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_VISIBILITY = 0x00000002;
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_IMPORTANCE = 0x00000004;
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_LIGHTS = 0x00000008;
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_VIBRATION = 0x00000010;
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_SOUND = 0x00000020;
 
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_ALLOWED = 0x00000040;
 
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_SHOW_BADGE = 0x00000080;
 
     /**
      * @hide
      */
-    @SystemApi
     public static final int USER_LOCKED_AUDIO_ATTRIBUTES = 0x00000100;
 
     /**
      * @hide
      */
-    @SystemApi
     public static final int[] LOCKABLE_FIELDS = new int[] {
             USER_LOCKED_PRIORITY,
             USER_LOCKED_VISIBILITY,
@@ -140,7 +134,8 @@
     private static final boolean DEFAULT_SHOW_BADGE = true;
 
     private final String mId;
-    private CharSequence mName;
+    private String mName;
+    private String mDesc;
     private int mImportance = DEFAULT_IMPORTANCE;
     private boolean mBypassDnd;
     private int mLockscreenVisibility = DEFAULT_VISIBILITY;
@@ -158,19 +153,19 @@
     /**
      * Creates a notification channel.
      *
-     * @param id The id of the channel. Must be unique per package.
-     * @param name The user visible name of the channel. Unchangeable once created; use this
-     *             constructor if the channel represents a user-defined category that does not
-     *             need to be translated. You can rename this channel when the system
+     * @param id The id of the channel. Must be unique per package. The value may be truncated if
+     *           it is too long.
+     * @param name The user visible name of the channel. You can rename this channel when the system
      *             locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
-     *             broadcast.
+     *             broadcast. The recommended maximum length is 40 characters; the value may be
+     *             truncated if it is too long.
      * @param importance The importance of the channel. This controls how interruptive notifications
      *                   posted to this channel are. See e.g.
      *                   {@link NotificationManager#IMPORTANCE_DEFAULT}.
      */
     public NotificationChannel(String id, CharSequence name, int importance) {
-        this.mId = id;
-        this.mName = name;
+        this.mId = getTrimmedString(id);
+        this.mName = name != null ? getTrimmedString(name.toString()) : null;
         this.mImportance = importance;
     }
 
@@ -180,7 +175,16 @@
         } else {
             mId = null;
         }
-        mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        if (in.readByte() != 0) {
+            mName = in.readString();
+        } else {
+            mName = null;
+        }
+        if (in.readByte() != 0) {
+            mDesc = in.readString();
+        } else {
+            mDesc = null;
+        }
         mImportance = in.readInt();
         mBypassDnd = in.readByte() != 0;
         mLockscreenVisibility = in.readInt();
@@ -212,7 +216,18 @@
         } else {
             dest.writeByte((byte) 0);
         }
-        TextUtils.writeToParcel(mName, dest, flags);
+        if (mName != null) {
+            dest.writeByte((byte) 1);
+            dest.writeString(mName);
+        } else {
+            dest.writeByte((byte) 0);
+        }
+        if (mDesc != null) {
+            dest.writeByte((byte) 1);
+            dest.writeString(mDesc);
+        } else {
+            dest.writeByte((byte) 0);
+        }
         dest.writeInt(mImportance);
         dest.writeByte(mBypassDnd ? (byte) 1 : (byte) 0);
         dest.writeInt(mLockscreenVisibility);
@@ -257,46 +272,33 @@
         mDeleted = deleted;
     }
 
+    // Modifiable by apps post channel creation
+
     /**
-     * Sets the name of this channel.
+     * Sets the user visible name of this channel.
+     *
+     * <p>The recommended maximum length is 40 characters; the value may be truncated if it is too
+     * long.
      */
     public void setName(CharSequence name) {
-        mName = name;
-    }
-
-    // Modifiable by a notification ranker.
-
-    /**
-     * Sets whether or not notifications posted to this channel can interrupt the user in
-     * {@link android.app.NotificationManager.Policy#INTERRUPTION_FILTER_PRIORITY} mode.
-     *
-     * Only modifiable by the system and notification ranker.
-     */
-    public void setBypassDnd(boolean bypassDnd) {
-        this.mBypassDnd = bypassDnd;
+        mName = name != null ? getTrimmedString(name.toString()) : null;
     }
 
     /**
-     * Sets whether notifications posted to this channel appear on the lockscreen or not, and if so,
-     * whether they appear in a redacted form. See e.g. {@link Notification#VISIBILITY_SECRET}.
+     * Sets the user visible description of this channel.
      *
-     * Only modifiable by the system and notification ranker.
+     * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too
+     * long.
      */
-    public void setLockscreenVisibility(int lockscreenVisibility) {
-        this.mLockscreenVisibility = lockscreenVisibility;
+    public void setDescription(String description) {
+        mDesc = getTrimmedString(description);
     }
 
-    /**
-     * Sets the level of interruption of this notification channel.
-     *
-     * Only modifiable by the system and notification ranker.
-     *
-     * @param importance the amount the user should be interrupted by notifications from this
-     *                   channel. See e.g.
-     *                   {@link android.app.NotificationManager#IMPORTANCE_DEFAULT}.
-     */
-    public void setImportance(int importance) {
-        this.mImportance = importance;
+    private String getTrimmedString(String input) {
+        if (input != null && input.length() > MAX_TEXT_LENGTH) {
+            return input.substring(0, MAX_TEXT_LENGTH);
+        }
+        return input;
     }
 
     // Modifiable by apps on channel creation.
@@ -385,6 +387,43 @@
     }
 
     /**
+     * Sets the level of interruption of this notification channel.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
+     *
+     * @param importance the amount the user should be interrupted by notifications from this
+     *                   channel. See e.g.
+     *                   {@link android.app.NotificationManager#IMPORTANCE_DEFAULT}.
+     */
+    public void setImportance(int importance) {
+        this.mImportance = importance;
+    }
+
+    // Modifiable by a notification ranker.
+
+    /**
+     * Sets whether or not notifications posted to this channel can interrupt the user in
+     * {@link android.app.NotificationManager.Policy#INTERRUPTION_FILTER_PRIORITY} mode.
+     *
+     * Only modifiable by the system and notification ranker.
+     */
+    public void setBypassDnd(boolean bypassDnd) {
+        this.mBypassDnd = bypassDnd;
+    }
+
+    /**
+     * Sets whether notifications posted to this channel appear on the lockscreen or not, and if so,
+     * whether they appear in a redacted form. See e.g. {@link Notification#VISIBILITY_SECRET}.
+     *
+     * Only modifiable by the system and notification ranker.
+     */
+    public void setLockscreenVisibility(int lockscreenVisibility) {
+        this.mLockscreenVisibility = lockscreenVisibility;
+    }
+
+
+    /**
      * Returns the id of this channel.
      */
     public String getId() {
@@ -394,11 +433,18 @@
     /**
      * Returns the user visible name of this channel.
      */
-    public @Nullable CharSequence getName() {
+    public CharSequence getName() {
         return mName;
     }
 
     /**
+     * Returns the user visible description of this channel.
+     */
+    public String getDescription() {
+        return mDesc;
+    }
+
+    /**
      * Returns the user specified importance {e.g. @link NotificationManager#IMPORTANCE_LOW} for
      * notifications posted to this channel.
      */
@@ -507,6 +553,7 @@
     @SystemApi
     public void populateFromXml(XmlPullParser parser) {
         // Name, id, and importance are set in the constructor.
+        setDescription(parser.getAttributeValue(null, ATT_DESC));
         setBypassDnd(Notification.PRIORITY_DEFAULT
                 != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT));
         setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
@@ -531,6 +578,9 @@
         if (getName() != null) {
             out.attribute(null, ATT_NAME, getName().toString());
         }
+        if (getDescription() != null) {
+            out.attribute(null, ATT_DESC, getDescription());
+        }
         if (getImportance() != DEFAULT_IMPORTANCE) {
             out.attribute(
                     null, ATT_IMPORTANCE, Integer.toString(getImportance()));
@@ -588,6 +638,7 @@
         JSONObject record = new JSONObject();
         record.put(ATT_ID, getId());
         record.put(ATT_NAME, getName());
+        record.put(ATT_DESC, getDescription());
         if (getImportance() != DEFAULT_IMPORTANCE) {
             record.put(ATT_IMPORTANCE,
                     NotificationListenerService.Ranking.importanceToString(getImportance()));
@@ -718,6 +769,10 @@
         if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
             return false;
         }
+        if (getDescription() != null ? !getDescription().equals(that.getDescription())
+                : that.getDescription() != null) {
+            return false;
+        }
         if (getSound() != null ? !getSound().equals(that.getSound()) : that.getSound() != null) {
             return false;
         }
@@ -734,6 +789,7 @@
     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 + getImportance();
         result = 31 * result + (mBypassDnd ? 1 : 0);
         result = 31 * result + getLockscreenVisibility();
@@ -755,6 +811,7 @@
         return "NotificationChannel{" +
                 "mId='" + mId + '\'' +
                 ", mName=" + mName +
+                ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") +
                 ", mImportance=" + mImportance +
                 ", mBypassDnd=" + mBypassDnd +
                 ", mLockscreenVisibility=" + mLockscreenVisibility +
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 2b0cd04..852af8a 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -40,6 +40,12 @@
  */
 public final class NotificationChannelGroup implements Parcelable {
 
+    /**
+     * The maximum length for text fields in a NotificationChannelGroup. Fields will be truncated at
+     * this limit.
+     */
+    private static final int MAX_TEXT_LENGTH = 1000;
+
     private static final String TAG_GROUP = "channelGroup";
     private static final String ATT_NAME = "name";
     private static final String ATT_ID = "id";
@@ -51,14 +57,16 @@
     /**
      * Creates a notification channel group.
      *
-     * @param id The id of the group. Must be unique per package.
+     * @param id The id of the group. Must be unique per package.  the value may be truncated if
+     *           it is too long.
      * @param name The user visible name of the group. You can rename this group when the system
      *             locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
-     *             broadcast.
+     *             broadcast. <p>The recommended maximum length is 40 characters; the value may be
+     *             truncated if it is too long.
      */
     public NotificationChannelGroup(String id, CharSequence name) {
-        this.mId = id;
-        this.mName = name;
+        this.mId = getTrimmedString(id);
+        this.mName = name != null ? getTrimmedString(name.toString()) : null;
     }
 
     protected NotificationChannelGroup(Parcel in) {
@@ -71,6 +79,13 @@
         in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader());
     }
 
+    private String getTrimmedString(String input) {
+        if (input != null && input.length() > MAX_TEXT_LENGTH) {
+            return input.substring(0, MAX_TEXT_LENGTH);
+        }
+        return input;
+    }
+
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         if (mId != null) {
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 0379970..097df31 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -421,8 +421,12 @@
     /**
      * Creates a notification channel that notifications can be posted to.
      *
-     * This can also be used to restore a deleted channel and to rename an existing channel. All
-     * other fields are ignored for channels that already exist.
+     * This can also be used to restore a deleted channel and to update an existing channel's
+     * name and description. 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
+     * named 'John Doe' that represents messages from a 'John Doe', and 'John Doe' changes his name
+     * to 'John Smith,' the channel can be renamed to match.
+     * 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
      *                 value. If the provided channel is malformed, a RemoteException will be
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index ce79465..a42a70e 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -201,7 +201,7 @@
                             // Channels
                             if (TAG_CHANNEL.equals(tagName)) {
                                 String id = parser.getAttributeValue(null, ATT_ID);
-                                CharSequence channelName = parser.getAttributeValue(null, ATT_NAME);
+                                String channelName = parser.getAttributeValue(null, ATT_NAME);
                                 int channelImportance =
                                         safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
                                 if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
@@ -546,7 +546,8 @@
                 existing.setDeleted(false);
             }
 
-            existing.setName(channel.getName());
+            existing.setName(channel.getName().toString());
+            existing.setDescription(channel.getDescription());
 
             MetricsLogger.action(getChannelLog(channel, pkg));
             updateConfig();
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 5a94018..af44264 100644
--- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
@@ -208,6 +208,7 @@
     private void compareChannels(NotificationChannel expected, NotificationChannel actual) {
         assertEquals(expected.getId(), actual.getId());
         assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
         assertEquals(expected.shouldVibrate(), actual.shouldVibrate());
         assertEquals(expected.shouldShowLights(), actual.shouldShowLights());
         assertEquals(expected.getImportance(), actual.getImportance());
@@ -283,6 +284,7 @@
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
         NotificationChannel channel2 =
                 new NotificationChannel("id2", "name2", IMPORTANCE_LOW);
+        channel2.setDescription("descriptions for all");
         channel2.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel2.enableLights(true);
         channel2.setBypassDnd(true);