Merge "Adds source to Notification Condition" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 5f7b0c2..f9f8d78 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -40545,19 +40545,26 @@
 
   public final class Condition implements android.os.Parcelable {
     ctor public Condition(android.net.Uri, String, int);
+    ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, int, int);
     ctor public Condition(android.net.Uri, String, String, String, int, int, int);
+    ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int);
     ctor public Condition(android.os.Parcel);
     method public android.service.notification.Condition copy();
     method public int describeContents();
     method public static boolean isValidId(android.net.Uri, String);
     method public static android.net.Uri.Builder newId(android.content.Context);
     method public static String relevanceToString(int);
+    method @FlaggedApi("android.app.modes_api") @NonNull public static String sourceToString(int);
     method public static String stateToString(int);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.Condition> CREATOR;
     field public static final int FLAG_RELEVANT_ALWAYS = 2; // 0x2
     field public static final int FLAG_RELEVANT_NOW = 1; // 0x1
     field public static final String SCHEME = "condition";
+    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_CONTEXT = 3; // 0x3
+    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_SCHEDULE = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_UNKNOWN = 0; // 0x0
+    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_USER_ACTION = 1; // 0x1
     field public static final int STATE_ERROR = 3; // 0x3
     field public static final int STATE_FALSE = 0; // 0x0
     field public static final int STATE_TRUE = 1; // 0x1
@@ -40567,6 +40574,7 @@
     field public final android.net.Uri id;
     field public final String line1;
     field public final String line2;
+    field @FlaggedApi("android.app.modes_api") public final int source;
     field public final int state;
     field public final String summary;
   }
diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java
index 4d33bfd..d76fa5b 100644
--- a/core/java/android/service/notification/Condition.java
+++ b/core/java/android/service/notification/Condition.java
@@ -16,8 +16,11 @@
 
 package android.service.notification;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Flags;
 import android.content.Context;
 import android.net.Uri;
 import android.os.Parcel;
@@ -57,7 +60,6 @@
      * Indicates that Do Not Disturb should be turned on.
      */
     public static final int STATE_TRUE = 1;
-
     public static final int STATE_UNKNOWN = 2;
     public static final int STATE_ERROR = 3;
 
@@ -90,6 +92,33 @@
     public final int flags;
     public final int icon;
 
+    /** @hide */
+    @IntDef(prefix = { "SOURCE_" }, value = {
+            SOURCE_UNKNOWN,
+            SOURCE_USER_ACTION,
+            SOURCE_SCHEDULE,
+            SOURCE_CONTEXT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Source {}
+
+    /** The state is changing due to an unknown reason. */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int SOURCE_UNKNOWN = 0;
+    /** The state is changing due to an explicit user action. */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int SOURCE_USER_ACTION = 1;
+    /** The state is changing due to an automatic schedule (alarm, set time, etc). */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int SOURCE_SCHEDULE = 2;
+    /** The state is changing due to a change in context (such as detected driving or sleeping). */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int SOURCE_CONTEXT = 3;
+
+    /** The source of, or reason for, the state change represented by this Condition. **/
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public final @Source int source;
+
     /**
      * The maximum string length for any string contained in this condition.
      * @hide
@@ -99,14 +128,48 @@
     /**
      * An object representing the current state of a {@link android.app.AutomaticZenRule}.
      * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
-     * @param summary a user visible description of the rule state.
+     * @param summary a user visible description of the rule state
+     * @param state whether the mode should be activated or deactivated
      */
+    // TODO: b/310208502 - Deprecate this in favor of constructor which specifies source.
     public Condition(Uri id, String summary, int state) {
-        this(id, summary, "", "", -1, state, FLAG_RELEVANT_ALWAYS);
+        this(id, summary, "", "", -1, state, SOURCE_UNKNOWN, FLAG_RELEVANT_ALWAYS);
     }
 
+    /**
+     * An object representing the current state of a {@link android.app.AutomaticZenRule}.
+     * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
+     * @param summary a user visible description of the rule state
+     * @param state whether the mode should be activated or deactivated
+     * @param source the source of, or reason for, the state change represented by this Condition
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public Condition(@Nullable Uri id, @Nullable String summary, @State int state,
+                     @Source int source) {
+        this(id, summary, "", "", -1, state, source, FLAG_RELEVANT_ALWAYS);
+    }
+
+    // TODO: b/310208502 - Deprecate this in favor of constructor which specifies source.
     public Condition(Uri id, String summary, String line1, String line2, int icon,
             int state, int flags) {
+        this(id, summary, line1, line2, icon, state, SOURCE_UNKNOWN, flags);
+    }
+
+    /**
+     * An object representing the current state of a {@link android.app.AutomaticZenRule}.
+     * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
+     * @param summary a user visible description of the rule state
+     * @param line1 a user-visible description of when the rule will end
+     * @param line2 a continuation of the user-visible description of when the rule will end
+     * @param icon an icon representing this condition
+     * @param state whether the mode should be activated or deactivated
+     * @param source the source of, or reason for, the state change represented by this Condition
+     * @param flags flags on this condition
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public Condition(@Nullable Uri id, @Nullable String summary, @Nullable String line1,
+                     @Nullable String line2, int icon, @State int state, @Source int source,
+                     int flags) {
         if (id == null) throw new IllegalArgumentException("id is required");
         if (summary == null) throw new IllegalArgumentException("summary is required");
         if (!isValidState(state)) throw new IllegalArgumentException("state is invalid: " + state);
@@ -116,6 +179,7 @@
         this.line2 = getTrimmedString(line2);
         this.icon = icon;
         this.state = state;
+        this.source = source;
         this.flags = flags;
     }
 
@@ -129,6 +193,7 @@
                 source.readString(),
                 source.readInt(),
                 source.readInt(),
+                Flags.modesApi() ? source.readInt() : SOURCE_UNKNOWN,
                 source.readInt());
     }
 
@@ -144,20 +209,27 @@
         dest.writeString(line2);
         dest.writeInt(icon);
         dest.writeInt(state);
+        if (Flags.modesApi()) {
+            dest.writeInt(this.source);
+        }
         dest.writeInt(this.flags);
     }
 
     @Override
     public String toString() {
-        return new StringBuilder(Condition.class.getSimpleName()).append('[')
+        StringBuilder sb = new StringBuilder(Condition.class.getSimpleName()).append('[')
                 .append("state=").append(stateToString(state))
                 .append(",id=").append(id)
                 .append(",summary=").append(summary)
                 .append(",line1=").append(line1)
                 .append(",line2=").append(line2)
-                .append(",icon=").append(icon)
-                .append(",flags=").append(flags)
+                .append(",icon=").append(icon);
+        if (Flags.modesApi()) {
+            sb.append(",source=").append(sourceToString(source));
+        }
+        return sb.append(",flags=").append(flags)
                 .append(']').toString();
+
     }
 
     /** @hide */
@@ -171,6 +243,7 @@
         proto.write(ConditionProto.LINE_2, line2);
         proto.write(ConditionProto.ICON, icon);
         proto.write(ConditionProto.STATE, state);
+        // TODO: b/310644464 - Add source to dump.
         proto.write(ConditionProto.FLAGS, flags);
 
         proto.end(token);
@@ -184,6 +257,16 @@
         throw new IllegalArgumentException("state is invalid: " + state);
     }
 
+    /** Provides a human-readable string version of the Source enum. */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static @NonNull String sourceToString(@Source int source) {
+        if (source == SOURCE_UNKNOWN) return "SOURCE_UNKNOWN";
+        if (source == SOURCE_USER_ACTION) return "SOURCE_USER_ACTION";
+        if (source == SOURCE_SCHEDULE) return "SOURCE_SCHEDULE";
+        if (source == SOURCE_CONTEXT) return "SOURCE_CONTEXT";
+        throw new IllegalArgumentException("source is invalid: " + source);
+    }
+
     public static String relevanceToString(int flags) {
         final boolean now = (flags & FLAG_RELEVANT_NOW) != 0;
         final boolean always = (flags & FLAG_RELEVANT_ALWAYS) != 0;
@@ -197,17 +280,24 @@
         if (!(o instanceof Condition)) return false;
         if (o == this) return true;
         final Condition other = (Condition) o;
-        return Objects.equals(other.id, id)
+        boolean finalEquals = Objects.equals(other.id, id)
                 && Objects.equals(other.summary, summary)
                 && Objects.equals(other.line1, line1)
                 && Objects.equals(other.line2, line2)
                 && other.icon == icon
                 && other.state == state
                 && other.flags == flags;
+        if (Flags.modesApi()) {
+            return finalEquals && other.source == source;
+        }
+        return finalEquals;
     }
 
     @Override
     public int hashCode() {
+        if (Flags.modesApi()) {
+            return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
+        }
         return Objects.hash(id, summary, line1, line2, icon, state, flags);
     }
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 305b751..fedad89 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -160,6 +160,7 @@
     private static final String CONDITION_ATT_LINE2 = "line2";
     private static final String CONDITION_ATT_ICON = "icon";
     private static final String CONDITION_ATT_STATE = "state";
+    private static final String CONDITION_ATT_SOURCE = "source";
     private static final String CONDITION_ATT_FLAGS = "flags";
 
     private static final String ZEN_POLICY_TAG = "zen_policy";
@@ -687,7 +688,12 @@
         final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
         final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
         try {
-            return new Condition(id, summary, line1, line2, icon, state, flags);
+            if (Flags.modesApi()) {
+                final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN);
+                return new Condition(id, summary, line1, line2, icon, state, source, flags);
+            } else {
+                return new Condition(id, summary, line1, line2, icon, state, flags);
+            }
         } catch (IllegalArgumentException e) {
             Slog.w(TAG, "Unable to read condition xml", e);
             return null;
@@ -701,6 +707,9 @@
         out.attribute(null, CONDITION_ATT_LINE2, c.line2);
         out.attributeInt(null, CONDITION_ATT_ICON, c.icon);
         out.attributeInt(null, CONDITION_ATT_STATE, c.state);
+        if (Flags.modesApi()) {
+            out.attributeInt(null, CONDITION_ATT_SOURCE, c.source);
+        }
         out.attributeInt(null, CONDITION_ATT_FLAGS, c.flags);
     }
 
diff --git a/core/tests/coretests/src/android/service/notification/ConditionTest.java b/core/tests/coretests/src/android/service/notification/ConditionTest.java
index 42629ba..612562e 100644
--- a/core/tests/coretests/src/android/service/notification/ConditionTest.java
+++ b/core/tests/coretests/src/android/service/notification/ConditionTest.java
@@ -16,17 +16,22 @@
 
 package android.service.notification;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.fail;
 
+import android.app.Flags;
 import android.net.Uri;
 import android.os.Parcel;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.google.common.base.Strings;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -37,8 +42,11 @@
 public class ConditionTest {
     private static final String CLASS = "android.service.notification.Condition";
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Test
-    public void testLongFields_inConstructors() {
+    public void testLongFields_inConstructors_classic() {
         String longString = Strings.repeat("A", 65536);
         Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530));
 
@@ -59,7 +67,7 @@
     }
 
     @Test
-    public void testLongFields_viaParcel() {
+    public void testLongFields_viaParcel_classic() {
         // Set fields via reflection to force them to be long, then parcel and unparcel to make sure
         // it gets truncated upon unparcelling.
         Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
@@ -98,4 +106,92 @@
         assertEquals(Condition.MAX_STRING_LENGTH, fromParcel.line1.length());
         assertEquals(Condition.MAX_STRING_LENGTH, fromParcel.line2.length());
     }
+
+    @Test
+    public void testLongFields_inConstructors() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        String longString = Strings.repeat("A", 65536);
+        Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530));
+
+        // Confirm strings are truncated via short constructor
+        Condition cond1 = new Condition(longUri, longString, Condition.STATE_TRUE,
+                Condition.SOURCE_CONTEXT);
+
+        assertThat(cond1.id.toString()).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(cond1.summary).hasLength(Condition.MAX_STRING_LENGTH);
+
+        // Confirm strings are truncated via long constructor
+        Condition cond2 = new Condition(longUri, longString, longString, longString,
+                -1, Condition.STATE_TRUE, Condition.SOURCE_CONTEXT, Condition.FLAG_RELEVANT_ALWAYS);
+
+        assertThat(cond2.id.toString()).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(cond2.summary).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(cond2.line1).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(cond2.line2).hasLength(Condition.MAX_STRING_LENGTH);
+    }
+
+    @Test
+    public void testLongFields_viaParcel() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        // Set fields via reflection to force them to be long, then parcel and unparcel to make sure
+        // it gets truncated upon unparcelling.
+        Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
+                Condition.STATE_TRUE, Condition.SOURCE_CONTEXT);
+
+        String longString = Strings.repeat("A", 65536);
+        Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530));
+        Field id = Class.forName(CLASS).getDeclaredField("id");
+        id.setAccessible(true);
+        id.set(cond, longUri);
+        Field summary = Class.forName(CLASS).getDeclaredField("summary");
+        summary.setAccessible(true);
+        summary.set(cond, longString);
+        Field line1 = Class.forName(CLASS).getDeclaredField("line1");
+        line1.setAccessible(true);
+        line1.set(cond, longString);
+        Field line2 = Class.forName(CLASS).getDeclaredField("line2");
+        line2.setAccessible(true);
+        line2.set(cond, longString);
+
+        Parcel parcel = Parcel.obtain();
+        cond.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        Condition fromParcel = new Condition(parcel);
+        assertThat(fromParcel.id.toString()).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(fromParcel.summary).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(fromParcel.line1).hasLength(Condition.MAX_STRING_LENGTH);
+        assertThat(fromParcel.line2).hasLength(Condition.MAX_STRING_LENGTH);
+    }
+
+    @Test
+    public void testEquals() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        Condition cond1 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
+                Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
+        Condition cond2 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
+                "", "", -1,
+                Condition.STATE_TRUE, Condition.SOURCE_SCHEDULE, Condition.FLAG_RELEVANT_ALWAYS);
+
+        assertThat(cond1).isNotEqualTo(cond2);
+        Condition cond3 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
+                Condition.STATE_TRUE, Condition.SOURCE_SCHEDULE);
+        assertThat(cond3).isEqualTo(cond2);
+    }
+
+    @Test
+    public void testParcelConstructor() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
+                Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
+
+        Parcel parcel = Parcel.obtain();
+        cond.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        Condition fromParcel = new Condition(parcel);
+        assertThat(fromParcel).isEqualTo(cond);
+    }
 }