Add new state to AutomaticZenRule

Test: ZenModeConfigTest
Test: (cts) NotificationManagerZenTest
Test: (cts) AutomaticZenRuleTest
Fixes: 308672543
Fixes: 308672010
Fixes: 308673538
Bug: 308674037
Change-Id: I66be27cce62a144289c5bd45c18bf6064ca2fe51
diff --git a/core/api/current.txt b/core/api/current.txt
index e8a6ac9..124f043 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -5315,11 +5315,15 @@
     method public android.net.Uri getConditionId();
     method @Nullable public android.content.ComponentName getConfigurationActivity();
     method public long getCreationTime();
+    method @FlaggedApi("android.app.modes_api") @DrawableRes public int getIconResId();
     method public int getInterruptionFilter();
     method public String getName();
     method public android.content.ComponentName getOwner();
+    method @FlaggedApi("android.app.modes_api") @Nullable public String getTriggerDescription();
+    method @FlaggedApi("android.app.modes_api") public int getType();
     method public android.service.notification.ZenPolicy getZenPolicy();
     method public boolean isEnabled();
+    method @FlaggedApi("android.app.modes_api") public boolean isManualInvocationAllowed();
     method public void setConditionId(android.net.Uri);
     method public void setConfigurationActivity(@Nullable android.content.ComponentName);
     method public void setEnabled(boolean);
@@ -5328,6 +5332,32 @@
     method public void setZenPolicy(android.service.notification.ZenPolicy);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.AutomaticZenRule> CREATOR;
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_BEDTIME = 3; // 0x3
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_DRIVING = 4; // 0x4
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_IMMERSIVE = 5; // 0x5
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_MANAGED = 7; // 0x7
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_OTHER = 0; // 0x0
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_TIME = 1; // 0x1
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_THEATER = 6; // 0x6
+    field @FlaggedApi("android.app.modes_api") public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+  }
+
+  @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
+    ctor public AutomaticZenRule.Builder(@NonNull android.app.AutomaticZenRule);
+    ctor public AutomaticZenRule.Builder(@NonNull String, @NonNull android.net.Uri);
+    method @NonNull public android.app.AutomaticZenRule build();
+    method @NonNull public android.app.AutomaticZenRule.Builder setConditionId(@NonNull android.net.Uri);
+    method @NonNull public android.app.AutomaticZenRule.Builder setConfigurationActivity(@Nullable android.content.ComponentName);
+    method @NonNull public android.app.AutomaticZenRule.Builder setEnabled(boolean);
+    method @NonNull public android.app.AutomaticZenRule.Builder setIconResId(@DrawableRes int);
+    method @NonNull public android.app.AutomaticZenRule.Builder setInterruptionFilter(int);
+    method @NonNull public android.app.AutomaticZenRule.Builder setManualInvocationAllowed(boolean);
+    method @NonNull public android.app.AutomaticZenRule.Builder setName(@NonNull String);
+    method @NonNull public android.app.AutomaticZenRule.Builder setOwner(@Nullable android.content.ComponentName);
+    method @NonNull public android.app.AutomaticZenRule.Builder setTriggerDescription(@Nullable String);
+    method @NonNull public android.app.AutomaticZenRule.Builder setType(int);
+    method @NonNull public android.app.AutomaticZenRule.Builder setZenPolicy(@Nullable android.service.notification.ZenPolicy);
   }
 
   public final class BackgroundServiceStartNotAllowedException extends android.app.ServiceStartNotAllowedException implements android.os.Parcelable {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index 7bfb1b5..919e084 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -16,16 +16,23 @@
 
 package android.app;
 
+import android.annotation.DrawableRes;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.NotificationManager.InterruptionFilter;
+import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.service.notification.Condition;
 import android.service.notification.ZenPolicy;
+import android.view.WindowInsetsController;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -36,7 +43,67 @@
     private static final int ENABLED = 1;
     /* @hide */
     private static final int DISABLED = 0;
-    private boolean enabled = false;
+
+    /**
+     * Rule is of an unknown type. This is the default value if not provided by the owning app.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_UNKNOWN = -1;
+    /**
+     * Rule is of a known type, but not one of the specific types.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_OTHER = 0;
+    /**
+     * The type for rules triggered according to a time-based schedule.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_SCHEDULE_TIME = 1;
+    /**
+     * The type for rules triggered by calendar events.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_SCHEDULE_CALENDAR = 2;
+    /**
+     * The type for rules triggered by bedtime/sleeping, like time of day, or snore detection.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_BEDTIME = 3;
+    /**
+     * The type for rules triggered by driving detection, like Bluetooth connections or vehicle
+     * sounds.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_DRIVING = 4;
+    /**
+     * The type for rules triggered by the user entering an immersive activity, like opening an app
+     * using {@link WindowInsetsController#hide(int)}.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_IMMERSIVE = 5;
+    /**
+     * The type for rules that have a {@link ZenPolicy} that implies that the
+     * device should not make sound and potentially hide some visual effects; may be triggered
+     * when entering a location where silence is requested, like a theater.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_THEATER = 6;
+    /**
+     * The type for rules created and managed by a device owner. These rules may not be fully
+     * editable by the device user.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int TYPE_MANAGED = 7;
+
+    /** @hide */
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_UNKNOWN, TYPE_OTHER, TYPE_SCHEDULE_TIME, TYPE_SCHEDULE_CALENDAR, TYPE_BEDTIME,
+            TYPE_DRIVING, TYPE_IMMERSIVE, TYPE_THEATER, TYPE_MANAGED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    private boolean enabled;
     private String name;
     private @InterruptionFilter int interruptionFilter;
     private Uri conditionId;
@@ -46,6 +113,10 @@
     private ZenPolicy mZenPolicy;
     private boolean mModified = false;
     private String mPkg;
+    private int mType = TYPE_UNKNOWN;
+    private int mIconResId;
+    private String mTriggerDescription;
+    private boolean mAllowManualInvocation;
 
     /**
      * The maximum string length for any string contained in this automatic zen rule. This pertains
@@ -55,6 +126,12 @@
     public static final int MAX_STRING_LENGTH = 1000;
 
     /**
+     * The maximum string length for the trigger description rule, given UI constraints.
+     * @hide
+     */
+    public static final int MAX_DESC_LENGTH = 150;
+
+    /**
      * Creates an automatic zen rule.
      *
      * @param name The name of the rule.
@@ -97,6 +174,7 @@
      *               action ({@link Condition#STATE_TRUE}).
      * @param enabled Whether the rule is enabled.
      */
+    // TODO (b/309088420): deprecate this constructor in favor of the builder
     public AutomaticZenRule(@NonNull String name, @Nullable ComponentName owner,
             @Nullable ComponentName configurationActivity, @NonNull Uri conditionId,
             @Nullable ZenPolicy policy, int interruptionFilter, boolean enabled) {
@@ -134,6 +212,12 @@
         mZenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
         mModified = source.readInt() == ENABLED;
         mPkg = source.readString();
+        if (Flags.modesApi()) {
+            mAllowManualInvocation = source.readBoolean();
+            mIconResId = source.readInt();
+            mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+            mType = source.readInt();
+        }
     }
 
     /**
@@ -269,6 +353,81 @@
         return mPkg;
     }
 
+    /**
+     * Gets the type of the rule.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Sets the type of the rule.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public void setType(@Type int type) {
+        mType = type;
+    }
+
+    /**
+     * Gets the user visible description of when this rule is active
+     * (see {@link Condition#STATE_TRUE}).
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @Nullable String getTriggerDescription() {
+        return mTriggerDescription;
+    }
+
+    /**
+     * Sets a user visible description of when this rule will be active
+     * (see {@link Condition#STATE_TRUE}).
+     *
+     * A description should be a (localized) string like "Mon-Fri, 9pm-7am" or
+     * "When connected to [Car Name]".
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public void setTriggerDescription(@Nullable String triggerDescription) {
+        mTriggerDescription = triggerDescription;
+    }
+
+    /**
+     * Gets the resource id of the drawable icon for this rule.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @DrawableRes int getIconResId() {
+        return mIconResId;
+    }
+
+    /**
+     * Sets a resource id of a tintable vector drawable representing the rule in image form.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public void setIconResId(int iconResId) {
+        mIconResId = iconResId;
+    }
+
+    /**
+     * Gets whether this rule can be manually activated by the user even when the triggering
+     * condition for the rule is not met.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean isManualInvocationAllowed() {
+        return mAllowManualInvocation;
+    }
+
+    /**
+     * Sets whether this rule can be manually activated by the user even when the triggering
+     * condition for the rule is not met.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public void setManualInvocationAllowed(boolean allowManualInvocation) {
+        mAllowManualInvocation = allowManualInvocation;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -291,11 +450,17 @@
         dest.writeParcelable(mZenPolicy, 0);
         dest.writeInt(mModified ? ENABLED : DISABLED);
         dest.writeString(mPkg);
+        if (Flags.modesApi()) {
+            dest.writeBoolean(mAllowManualInvocation);
+            dest.writeInt(mIconResId);
+            dest.writeString(mTriggerDescription);
+            dest.writeInt(mType);
+        }
     }
 
     @Override
     public String toString() {
-        return new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[')
+        StringBuilder sb = new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[')
                 .append("enabled=").append(enabled)
                 .append(",name=").append(name)
                 .append(",interruptionFilter=").append(interruptionFilter)
@@ -304,8 +469,16 @@
                 .append(",owner=").append(owner)
                 .append(",configActivity=").append(configurationActivity)
                 .append(",creationTime=").append(creationTime)
-                .append(",mZenPolicy=").append(mZenPolicy)
-                .append(']').toString();
+                .append(",mZenPolicy=").append(mZenPolicy);
+
+        if (Flags.modesApi()) {
+            sb.append(",allowManualInvocation=").append(mAllowManualInvocation)
+                    .append(",iconResId=").append(mIconResId)
+                    .append(",triggerDescription=").append(mTriggerDescription)
+                    .append(",type=").append(mType);
+        }
+
+        return sb.append(']').toString();
     }
 
     @Override
@@ -313,7 +486,7 @@
         if (!(o instanceof AutomaticZenRule)) return false;
         if (o == this) return true;
         final AutomaticZenRule other = (AutomaticZenRule) o;
-        return other.enabled == enabled
+        boolean finalEquals = other.enabled == enabled
                 && other.mModified == mModified
                 && Objects.equals(other.name, name)
                 && other.interruptionFilter == interruptionFilter
@@ -323,10 +496,23 @@
                 && Objects.equals(other.configurationActivity, configurationActivity)
                 && Objects.equals(other.mPkg, mPkg)
                 && other.creationTime == creationTime;
+        if (Flags.modesApi()) {
+            return finalEquals
+                    && other.mAllowManualInvocation == mAllowManualInvocation
+                    && other.mIconResId == mIconResId
+                    && Objects.equals(other.mTriggerDescription, mTriggerDescription)
+                    && other.mType == mType;
+        }
+        return finalEquals;
     }
 
     @Override
     public int hashCode() {
+        if (Flags.modesApi()) {
+            return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
+                    configurationActivity, mZenPolicy, mModified, creationTime, mPkg,
+                    mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
+        }
         return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
                 configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
     }
@@ -357,8 +543,12 @@
      * Returns a truncated copy of the string if the string is longer than MAX_STRING_LENGTH.
      */
     private static String getTrimmedString(String input) {
-        if (input != null && input.length() > MAX_STRING_LENGTH) {
-            return input.substring(0, MAX_STRING_LENGTH);
+        return getTrimmedString(input, MAX_STRING_LENGTH);
+    }
+
+    private static String getTrimmedString(String input, int length) {
+        if (input != null && input.length() > length) {
+            return input.substring(0, length);
         }
         return input;
     }
@@ -373,4 +563,138 @@
         }
         return input;
     }
+
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final class Builder {
+        private String mName;
+        private ComponentName mOwner;
+        private Uri mConditionId;
+        private int mInterruptionFilter;
+        private boolean mEnabled;
+        private ComponentName mConfigurationActivity = null;
+        private ZenPolicy mPolicy = null;
+        private int mType;
+        private String mDescription;
+        private int mIconResId;
+        private boolean mAllowManualInvocation;
+        private long mCreationTime;
+        private String mPkg;
+
+        public Builder(@NonNull AutomaticZenRule rule) {
+            mName = rule.getName();
+            mOwner = rule.getOwner();
+            mConditionId = rule.getConditionId();
+            mInterruptionFilter = rule.getInterruptionFilter();
+            mEnabled = rule.isEnabled();
+            mConfigurationActivity = rule.getConfigurationActivity();
+            mPolicy = rule.getZenPolicy();
+            mType = rule.getType();
+            mDescription = rule.getTriggerDescription();
+            mIconResId = rule.getIconResId();
+            mAllowManualInvocation = rule.isManualInvocationAllowed();
+            mCreationTime = rule.getCreationTime();
+            mPkg = rule.getPackageName();
+        }
+
+        public Builder(@NonNull String name, @NonNull Uri conditionId) {
+            mName = name;
+            mConditionId = conditionId;
+        }
+
+        public @NonNull Builder setName(@NonNull String name) {
+            mName = name;
+            return this;
+        }
+
+        public @NonNull Builder setOwner(@Nullable ComponentName owner) {
+            mOwner = owner;
+            return this;
+        }
+
+        public @NonNull Builder setConditionId(@NonNull Uri conditionId) {
+            mConditionId = conditionId;
+            return this;
+        }
+
+        public @NonNull Builder setInterruptionFilter(
+                @InterruptionFilter int interruptionFilter) {
+            mInterruptionFilter = interruptionFilter;
+            return this;
+        }
+
+        public @NonNull Builder setEnabled(boolean enabled) {
+            mEnabled = enabled;
+            return this;
+        }
+
+        public @NonNull Builder setConfigurationActivity(
+                @Nullable ComponentName configurationActivity) {
+            mConfigurationActivity = configurationActivity;
+            return this;
+        }
+
+        public @NonNull Builder setZenPolicy(@Nullable ZenPolicy policy) {
+            mPolicy = policy;
+            return this;
+        }
+
+        /**
+         * Sets the type of the rule
+         */
+        public @NonNull Builder setType(@Type int type) {
+            mType = type;
+            return this;
+        }
+
+        /**
+         * Sets a user visible description of when this rule will be active
+         * (see {@link Condition#STATE_TRUE}).
+         *
+         * A description should be a (localized) string like "Mon-Fri, 9pm-7am" or
+         * "When connected to [Car Name]".
+         */
+        public @NonNull Builder setTriggerDescription(@Nullable String description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Sets a resource id of a tintable vector drawable representing the rule in image form.
+         */
+        public @NonNull Builder setIconResId(@DrawableRes int iconResId) {
+            mIconResId = iconResId;
+            return this;
+        }
+
+        /**
+         * Sets whether this rule can be manually activated by the user even when the triggering
+         * condition for the rule is not met.
+         */
+        public @NonNull Builder setManualInvocationAllowed(boolean allowManualInvocation) {
+            mAllowManualInvocation = allowManualInvocation;
+            return this;
+        }
+
+        /**
+         * Sets the time at which this rule was created, in milliseconds since epoch
+         * @hide
+         */
+        public @NonNull Builder setCreationTime(long creationTime) {
+            mCreationTime = creationTime;
+            return this;
+        }
+
+        public @NonNull AutomaticZenRule build() {
+            AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity,
+                    mConditionId, mPolicy, mInterruptionFilter, mEnabled);
+            rule.creationTime = mCreationTime;
+            rule.mType = mType;
+            rule.mTriggerDescription = mDescription;
+            rule.mIconResId = mIconResId;
+            rule.mAllowManualInvocation = mAllowManualInvocation;
+            rule.setPackageName(mPkg);
+
+            return rule;
+        }
+    }
 }
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 828c062..ff4dfc7 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -28,6 +28,8 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
+import android.app.AutomaticZenRule;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.app.NotificationManager.Policy;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -177,6 +179,10 @@
     private static final String RULE_ATT_CREATION_TIME = "creationTime";
     private static final String RULE_ATT_ENABLER = "enabler";
     private static final String RULE_ATT_MODIFIED = "modified";
+    private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable";
+    private static final String RULE_ATT_TYPE = "type";
+    private static final String RULE_ATT_ICON = "rule_icon";
+    private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
 
     @UnsupportedAppUsage
     public boolean allowAlarms = DEFAULT_ALLOW_ALARMS;
@@ -212,7 +218,7 @@
         allowCallsFrom = source.readInt();
         allowMessagesFrom = source.readInt();
         user = source.readInt();
-        manualRule = source.readParcelable(null, android.service.notification.ZenModeConfig.ZenRule.class);
+        manualRule = source.readParcelable(null, ZenRule.class);
         final int len = source.readInt();
         if (len > 0) {
             final String[] ids = new String[len];
@@ -622,6 +628,12 @@
         }
         rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false);
         rt.zenPolicy = readZenPolicyXml(parser);
+        if (Flags.modesApi()) {
+            rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
+            rt.iconResId = safeInt(parser, RULE_ATT_ICON, 0);
+            rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
+            rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
+        }
         return rt;
     }
 
@@ -655,6 +667,14 @@
             writeZenPolicyXml(rule.zenPolicy, out);
         }
         out.attributeBoolean(null, RULE_ATT_MODIFIED, rule.modified);
+        if (Flags.modesApi()) {
+            out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
+            out.attributeInt(null, RULE_ATT_ICON, rule.iconResId);
+            if (rule.triggerDescription != null) {
+                out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
+            }
+            out.attributeInt(null, RULE_ATT_TYPE, rule.type);
+        }
     }
 
     public static Condition readConditionXml(TypedXmlPullParser parser) {
@@ -1726,6 +1746,11 @@
         public ZenPolicy zenPolicy;
         public boolean modified;    // rule has been modified from initial creation
         public String pkg;
+        public int type = AutomaticZenRule.TYPE_UNKNOWN;
+        public String triggerDescription;
+        // TODO (b/308672670): switch to string res name
+        public int iconResId;
+        public boolean allowManualInvocation;
 
         public ZenRule() { }
 
@@ -1750,6 +1775,12 @@
             zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
             modified = source.readInt() == 1;
             pkg = source.readString();
+            if (Flags.modesApi()) {
+                allowManualInvocation = source.readBoolean();
+                iconResId = source.readInt();
+                triggerDescription = source.readString();
+                type = source.readInt();
+            }
         }
 
         @Override
@@ -1788,11 +1819,17 @@
             dest.writeParcelable(zenPolicy, 0);
             dest.writeInt(modified ? 1 : 0);
             dest.writeString(pkg);
+            if (Flags.modesApi()) {
+                dest.writeBoolean(allowManualInvocation);
+                dest.writeInt(iconResId);
+                dest.writeString(triggerDescription);
+                dest.writeInt(type);
+            }
         }
 
         @Override
         public String toString() {
-            return new StringBuilder(ZenRule.class.getSimpleName()).append('[')
+            StringBuilder sb = new StringBuilder(ZenRule.class.getSimpleName()).append('[')
                     .append("id=").append(id)
                     .append(",state=").append(condition == null ? "STATE_FALSE"
                             : Condition.stateToString(condition.state))
@@ -1808,8 +1845,16 @@
                     .append(",enabler=").append(enabler)
                     .append(",zenPolicy=").append(zenPolicy)
                     .append(",modified=").append(modified)
-                    .append(",condition=").append(condition)
-                    .append(']').toString();
+                    .append(",condition=").append(condition);
+
+            if (Flags.modesApi()) {
+                sb.append(",allowManualInvocation=").append(allowManualInvocation)
+                        .append(",iconResId=").append(iconResId)
+                        .append(",triggerDescription=").append(triggerDescription)
+                        .append(",type=").append(type);
+            }
+
+            return sb.append(']').toString();
         }
 
         /** @hide */
@@ -1845,7 +1890,7 @@
             if (!(o instanceof ZenRule)) return false;
             if (o == this) return true;
             final ZenRule other = (ZenRule) o;
-            return other.enabled == enabled
+            boolean finalEquals = other.enabled == enabled
                     && other.snoozing == snoozing
                     && Objects.equals(other.name, name)
                     && other.zenMode == zenMode
@@ -1858,10 +1903,25 @@
                     && Objects.equals(other.zenPolicy, zenPolicy)
                     && Objects.equals(other.pkg, pkg)
                     && other.modified == modified;
+
+            if (Flags.modesApi()) {
+                return finalEquals
+                        && other.allowManualInvocation == allowManualInvocation
+                        && other.iconResId == iconResId
+                        && Objects.equals(other.triggerDescription, triggerDescription)
+                        && other.type == type;
+            }
+
+            return finalEquals;
         }
 
         @Override
         public int hashCode() {
+            if (Flags.modesApi()) {
+                return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+                        component, configurationActivity, pkg, id, enabler, zenPolicy, modified,
+                        allowManualInvocation, iconResId, triggerDescription, type);
+            }
             return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                     component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
         }
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index a4f129e..eb55e40 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -454,6 +454,11 @@
         public static final String FIELD_ZEN_POLICY = "zenPolicy";
         public static final String FIELD_MODIFIED = "modified";
         public static final String FIELD_PKG = "pkg";
+        public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation";
+        public static final String FIELD_ICON_RES = "iconResId";
+        public static final String FIELD_TRIGGER = "triggerDescription";
+        public static final String FIELD_TYPE = "type";
+        // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
 
         // Special field to track whether this rule became active or inactive
         FieldDiff<Boolean> mActiveDiff;
@@ -529,6 +534,20 @@
             if (!Objects.equals(from.pkg, to.pkg)) {
                 addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
             }
+            if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
+                addField(FIELD_TRIGGER,
+                        new FieldDiff<>(from.triggerDescription, to.triggerDescription));
+            }
+            if (from.type != to.type) {
+                addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
+            }
+            if (from.allowManualInvocation != to.allowManualInvocation) {
+                addField(FIELD_ALLOW_MANUAL,
+                        new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
+            }
+            if (!Objects.equals(from.iconResId, to.iconResId)) {
+                addField(FIELD_ICON_RES, new FieldDiff(from.iconResId, to.iconResId));
+            }
         }
 
         /**
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 282fdad..ba2ea88 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -125,6 +125,9 @@
             Field configActivity = Class.forName(CLASS).getDeclaredField("configurationActivity");
             configActivity.setAccessible(true);
             configActivity.set(rule, new ComponentName(longString, longString));
+            Field trigger = Class.forName(CLASS).getDeclaredField("mTriggerDescription");
+            trigger.setAccessible(true);
+            trigger.set(rule, longString);
         } catch (NoSuchFieldException e) {
             fail(e.toString());
         } catch (ClassNotFoundException e) {
@@ -149,5 +152,6 @@
                 fromParcel.getOwner().getPackageName().length());
         assertEquals(AutomaticZenRule.MAX_STRING_LENGTH,
                 fromParcel.getOwner().getClassName().length());
+        assertEquals(AutomaticZenRule.MAX_DESC_LENGTH, rule.getTriggerDescription().length());
     }
 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index a14f3fe..4e49c6e 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -199,6 +199,7 @@
         "biometrics_flags_lib",
         "am_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "android.app.flags-aconfig-java"
     ],
     javac_shard_size: 50,
     javacflags: [
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 71562dc..9802adf 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -26,6 +26,7 @@
 
 import android.app.AppOpsManager;
 import android.app.AutomaticZenRule;
+import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.NotificationManager.Policy;
@@ -670,14 +671,37 @@
         if (rule.enabled != automaticZenRule.isEnabled()) {
             rule.snoozing = false;
         }
+        if (Flags.modesApi()) {
+            rule.allowManualInvocation = automaticZenRule.isManualInvocationAllowed();
+            rule.iconResId = automaticZenRule.getIconResId();
+            rule.triggerDescription = automaticZenRule.getTriggerDescription();
+            rule.type = automaticZenRule.getType();
+        }
     }
 
     protected AutomaticZenRule createAutomaticZenRule(ZenRule rule) {
-        AutomaticZenRule azr =  new AutomaticZenRule(rule.name, rule.component,
-                rule.configurationActivity,
-                rule.conditionId, rule.zenPolicy,
-                NotificationManager.zenModeToInterruptionFilter(rule.zenMode),
-                rule.enabled, rule.creationTime);
+        AutomaticZenRule azr;
+        if (Flags.modesApi()) {
+            azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
+                    .setManualInvocationAllowed(rule.allowManualInvocation)
+                    .setCreationTime(rule.creationTime)
+                    .setIconResId(rule.iconResId)
+                    .setType(rule.type)
+                    .setZenPolicy(rule.zenPolicy)
+                    .setEnabled(rule.enabled)
+                    .setInterruptionFilter(
+                            NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
+                    .setOwner(rule.component)
+                    .setConfigurationActivity(rule.configurationActivity)
+                    .setTriggerDescription(rule.triggerDescription)
+                    .build();
+        } else {
+            azr = new AutomaticZenRule(rule.name, rule.component,
+                    rule.configurationActivity,
+                    rule.conditionId, rule.zenPolicy,
+                    NotificationManager.zenModeToInterruptionFilter(rule.zenMode),
+                    rule.enabled, rule.creationTime);
+        }
         azr.setPackageName(rule.pkg);
         return azr;
     }
@@ -713,6 +737,9 @@
                 newRule.zenMode = zenMode;
                 newRule.conditionId = conditionId;
                 newRule.enabler = caller;
+                if (Flags.modesApi()) {
+                    newRule.allowManualInvocation = true;
+                }
                 newConfig.manualRule = newRule;
             }
             setConfigLocked(newConfig, reason, null, setRingerMode, callingUid,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 3ba9400..261b5d3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.notification;
 
+import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 
 import static junit.framework.TestCase.assertEquals;
@@ -24,9 +25,12 @@
 import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 
+import android.app.Flags;
 import android.app.NotificationManager.Policy;
 import android.content.ComponentName;
 import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.service.notification.Condition;
 import android.service.notification.ZenModeConfig;
@@ -41,6 +45,7 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParserException;
@@ -55,6 +60,28 @@
 @RunWith(AndroidJUnit4.class)
 public class ZenModeConfigTest extends UiServiceTestCase {
 
+    private final String NAME = "name";
+    private final ComponentName OWNER = new ComponentName("pkg", "cls");
+    private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
+    private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
+    private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
+            .authority("authority")
+            .appendPath("path")
+            .appendPath("test")
+            .build();
+
+    private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE);
+    private final String TRIGGER_DESC = "Every Night, 10pm to 6am";
+    private final int TYPE = TYPE_BEDTIME;
+    private final boolean ALLOW_MANUAL = true;
+    private final int ICON_RES_ID = 1234;
+    private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
+    private final boolean ENABLED = true;
+    private final int CREATION_TIME = 123;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Test
     public void testPriorityOnlyMutingAllNotifications() {
         ZenModeConfig config = getMutedRingerConfig();
@@ -202,7 +229,59 @@
     }
 
     @Test
-    public void testRuleXml() throws Exception {
+    public void testWriteToParcel() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.configurationActivity = CONFIG_ACTIVITY;
+        rule.component = OWNER;
+        rule.conditionId = CONDITION_ID;
+        rule.condition = CONDITION;
+        rule.enabled = ENABLED;
+        rule.creationTime = 123;
+        rule.id = "id";
+        rule.zenMode = INTERRUPTION_FILTER;
+        rule.modified = true;
+        rule.name = NAME;
+        rule.snoozing = true;
+        rule.pkg = OWNER.getPackageName();
+        rule.zenPolicy = POLICY;
+
+        rule.allowManualInvocation = ALLOW_MANUAL;
+        rule.type = TYPE;
+        rule.iconResId = ICON_RES_ID;
+        rule.triggerDescription = TRIGGER_DESC;
+
+        Parcel parcel = Parcel.obtain();
+        rule.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ZenModeConfig.ZenRule parceled = new ZenModeConfig.ZenRule(parcel);
+
+        assertEquals(rule.pkg, parceled.pkg);
+        assertEquals(rule.snoozing, parceled.snoozing);
+        assertEquals(rule.enabler, parceled.enabler);
+        assertEquals(rule.component, parceled.component);
+        assertEquals(rule.configurationActivity, parceled.configurationActivity);
+        assertEquals(rule.condition, parceled.condition);
+        assertEquals(rule.enabled, parceled.enabled);
+        assertEquals(rule.creationTime, parceled.creationTime);
+        assertEquals(rule.modified, parceled.modified);
+        assertEquals(rule.conditionId, parceled.conditionId);
+        assertEquals(rule.name, parceled.name);
+        assertEquals(rule.zenMode, parceled.zenMode);
+
+        assertEquals(rule.allowManualInvocation, parceled.allowManualInvocation);
+        assertEquals(rule.iconResId, parceled.iconResId);
+        assertEquals(rule.type, parceled.type);
+        assertEquals(rule.triggerDescription, parceled.triggerDescription);
+        assertEquals(rule.zenPolicy, parceled.zenPolicy);
+        assertEquals(rule, parceled);
+        assertEquals(rule.hashCode(), parceled.hashCode());
+
+    }
+
+    @Test
+    public void testRuleXml_classic() throws Exception {
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = new ComponentName("a", "a");
         rule.component = new ComponentName("b", "b");
@@ -239,6 +318,58 @@
     }
 
     @Test
+    public void testRuleXml() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.configurationActivity = CONFIG_ACTIVITY;
+        rule.component = OWNER;
+        rule.conditionId = CONDITION_ID;
+        rule.condition = CONDITION;
+        rule.enabled = ENABLED;
+        rule.creationTime = 123;
+        rule.id = "id";
+        rule.zenMode = INTERRUPTION_FILTER;
+        rule.modified = true;
+        rule.name = NAME;
+        rule.snoozing = true;
+        rule.pkg = OWNER.getPackageName();
+        rule.zenPolicy = POLICY;
+        rule.creationTime = CREATION_TIME;
+
+        rule.allowManualInvocation = ALLOW_MANUAL;
+        rule.type = TYPE;
+        rule.iconResId = ICON_RES_ID;
+        rule.triggerDescription = TRIGGER_DESC;
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
+
+        assertEquals(rule.pkg, fromXml.pkg);
+        // always resets on reboot
+        assertFalse(fromXml.snoozing);
+        //should all match original
+        assertEquals(rule.component, fromXml.component);
+        assertEquals(rule.configurationActivity, fromXml.configurationActivity);
+        assertNull(fromXml.enabler);
+        assertEquals(rule.condition, fromXml.condition);
+        assertEquals(rule.enabled, fromXml.enabled);
+        assertEquals(rule.creationTime, fromXml.creationTime);
+        assertEquals(rule.modified, fromXml.modified);
+        assertEquals(rule.conditionId, fromXml.conditionId);
+        assertEquals(rule.name, fromXml.name);
+        assertEquals(rule.zenMode, fromXml.zenMode);
+        assertEquals(rule.creationTime, fromXml.creationTime);
+
+        assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation);
+        assertEquals(rule.type, fromXml.type);
+        assertEquals(rule.triggerDescription, fromXml.triggerDescription);
+        assertEquals(rule.iconResId, fromXml.iconResId);
+    }
+
+    @Test
     public void testRuleXml_pkg_component() throws Exception {
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = new ComponentName("a", "a");
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index bcd807a..fd3d5e9b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -23,6 +23,7 @@
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
 
+import android.app.AutomaticZenRule;
 import android.content.ComponentName;
 import android.net.Uri;
 import android.provider.Settings;
@@ -229,6 +230,10 @@
         rule.name = "name";
         rule.snoozing = true;
         rule.pkg = "a";
+        rule.allowManualInvocation = true;
+        rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+        rule.iconResId = 123;
+        rule.triggerDescription = "At night";
         return rule;
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index e22c104..0349ad9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.notification;
 
+import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
@@ -71,6 +72,7 @@
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.AutomaticZenRule;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.app.NotificationManager.Policy;
 import android.content.ComponentName;
@@ -88,6 +90,7 @@
 import android.net.Uri;
 import android.os.Process;
 import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.service.notification.Condition;
@@ -120,6 +123,7 @@
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -150,6 +154,28 @@
     private static final int CUSTOM_PKG_UID = 1;
     private static final String CUSTOM_RULE_ID = "custom_rule";
 
+    private final String NAME = "name";
+    private final ComponentName OWNER = new ComponentName("pkg", "cls");
+    private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
+    private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
+    private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
+            .authority("authority")
+            .appendPath("path")
+            .appendPath("test")
+            .build();
+
+    private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE);
+    private final String TRIGGER_DESC = "Every Night, 10pm to 6am";
+    private final int TYPE = TYPE_BEDTIME;
+    private final boolean ALLOW_MANUAL = true;
+    private final int ICON_RES_ID = 1234;
+    private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
+    private final boolean ENABLED = true;
+    private final int CREATION_TIME = 123;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     ConditionProviders mConditionProviders;
     @Mock NotificationManager mNotificationManager;
     @Mock PackageManager mPackageManager;
@@ -1961,6 +1987,26 @@
 
     @Test
     public void testSetManualZenMode() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        setupZenConfig();
+
+        // note that caller=null because that's how it comes in from NMS.setZenMode
+        mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "",
+                Process.SYSTEM_UID, true);
+
+        // confirm that setting zen mode via setManualZenMode changed the zen mode correctly
+        assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode);
+        assertEquals(true, mZenModeHelper.mConfig.manualRule.allowManualInvocation);
+
+        // and also that it works to turn it back off again
+        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+                Process.SYSTEM_UID, true);
+
+        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+    }
+
+    @Test
+    public void testSetManualZenMode_legacy() {
         setupZenConfig();
 
         // note that caller=null because that's how it comes in from NMS.setZenMode
@@ -2607,6 +2653,47 @@
         assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking());  // custom stricter
     }
 
+    @Test
+    public void testCreateAutomaticZenRule_allFields() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.configurationActivity = CONFIG_ACTIVITY;
+        rule.component = OWNER;
+        rule.conditionId = CONDITION_ID;
+        rule.condition = CONDITION;
+        rule.enabled = ENABLED;
+        rule.creationTime = 123;
+        rule.id = "id";
+        rule.zenMode = INTERRUPTION_FILTER;
+        rule.modified = true;
+        rule.name = NAME;
+        rule.snoozing = true;
+        rule.pkg = OWNER.getPackageName();
+        rule.zenPolicy = POLICY;
+
+        rule.allowManualInvocation = ALLOW_MANUAL;
+        rule.type = TYPE;
+        rule.iconResId = ICON_RES_ID;
+        rule.triggerDescription = TRIGGER_DESC;
+
+        AutomaticZenRule actual = mZenModeHelper.createAutomaticZenRule(rule);
+
+        assertEquals(NAME, actual.getName());
+        assertEquals(OWNER, actual.getOwner());
+        assertEquals(CONDITION_ID, actual.getConditionId());
+        assertEquals(NotificationManager.INTERRUPTION_FILTER_ALARMS,
+                actual.getInterruptionFilter());
+        assertEquals(ENABLED, actual.isEnabled());
+        assertEquals(POLICY, actual.getZenPolicy());
+        assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity());
+        assertEquals(TYPE, actual.getType());
+        assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed());
+        assertEquals(CREATION_TIME, actual.getCreationTime());
+        assertEquals(OWNER.getPackageName(), actual.getPackageName());
+        assertEquals(ICON_RES_ID, actual.getIconResId());
+        assertEquals(TRIGGER_DESC, actual.getTriggerDescription());
+    }
+
     private void setupZenConfig() {
         mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
         mZenModeHelper.mConfig.allowAlarms = false;