Merge "Start widget configuration activity if needed" into main
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 50c9fd3..ef1fa60 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -912,7 +912,7 @@
 }
 
 // This module can be built with:
-// m out/soong/.intermediates/frameworks/base/api_versions_module_lib/android_common/metalava/api-versions.xml
+// m out/soong/.intermediates/frameworks/base/api/api_versions_module_lib/android_common/metalava/api-versions.xml
 droidstubs {
     name: "api_versions_module_lib",
     srcs: [":android_module_stubs_current_with_test_libs{.jar}"],
diff --git a/core/api/current.txt b/core/api/current.txt
index 88eea95..e0b224e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -5316,6 +5316,7 @@
     ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean);
     ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
     ctor public AutomaticZenRule(android.os.Parcel);
+    method @FlaggedApi("android.app.modes_api") public boolean canUpdate();
     method public int describeContents();
     method public android.net.Uri getConditionId();
     method @Nullable public android.content.ComponentName getConfigurationActivity();
@@ -12369,7 +12370,6 @@
 
   public final class ModuleInfo implements android.os.Parcelable {
     method public int describeContents();
-    method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @NonNull public java.util.Collection<java.lang.String> getApkInApexPackageNames();
     method @Nullable public CharSequence getName();
     method @Nullable public String getPackageName();
     method public boolean isHidden();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 572be19..d2af9db 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -284,6 +284,16 @@
     method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int);
   }
 
+  public final class AutomaticZenRule implements android.os.Parcelable {
+    method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_INTERRUPTION_FILTER = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_NAME = 1; // 0x1
+  }
+
+  @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.app.AutomaticZenRule.Builder setUserModifiedFields(int);
+  }
+
   public class BroadcastOptions extends android.app.ComponentOptions {
     ctor public BroadcastOptions();
     ctor public BroadcastOptions(@NonNull android.os.Bundle);
@@ -3007,6 +3017,49 @@
     method @Deprecated public boolean isBound();
   }
 
+  @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+    method public int getUserModifiedFields();
+    field public static final int FIELD_DIM_WALLPAPER = 4; // 0x4
+    field public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 16; // 0x10
+    field public static final int FIELD_DISABLE_TAP_TO_WAKE = 32; // 0x20
+    field public static final int FIELD_DISABLE_TILT_TO_WAKE = 64; // 0x40
+    field public static final int FIELD_DISABLE_TOUCH = 128; // 0x80
+    field public static final int FIELD_GRAYSCALE = 1; // 0x1
+    field public static final int FIELD_MAXIMIZE_DOZE = 512; // 0x200
+    field public static final int FIELD_MINIMIZE_RADIO_USAGE = 256; // 0x100
+    field public static final int FIELD_NIGHT_MODE = 8; // 0x8
+    field public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 2; // 0x2
+  }
+
+  @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+    method @NonNull public android.service.notification.ZenDeviceEffects.Builder setUserModifiedFields(int);
+  }
+
+  public final class ZenPolicy implements android.os.Parcelable {
+    method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_ALLOW_CHANNELS = 8; // 0x8
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_CALLS = 2; // 0x2
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_CONVERSATIONS = 4; // 0x4
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_MESSAGES = 1; // 0x1
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 128; // 0x80
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 32; // 0x20
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 256; // 0x100
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 64; // 0x40
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 512; // 0x200
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_AMBIENT = 32768; // 0x8000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_BADGE = 16384; // 0x4000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1024; // 0x400
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_LIGHTS = 2048; // 0x800
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 65536; // 0x10000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_PEEK = 4096; // 0x1000
+    field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 8192; // 0x2000
+  }
+
+  public static final class ZenPolicy.Builder {
+    ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy);
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder setUserModifiedFields(int);
+  }
+
 }
 
 package android.service.quickaccesswallet {
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index bf26bd0..5e904ef9 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -535,6 +535,10 @@
     Missing nullability on parameter `foreground` in method `isDefaultFocusHighlightNeeded`
 
 
+OptionalBuilderConstructorArgument: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy) parameter #0:
+    Builder constructor arguments must be mandatory (i.e. not @Nullable): parameter policy in android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy policy)
+
+
 ProtectedMember: android.app.AppDetailsActivity#onCreate(android.os.Bundle):
     Protected methods not allowed; must be public: method android.app.AppDetailsActivity.onCreate(android.os.Bundle)}
 ProtectedMember: android.view.ViewGroup#resetResolvedDrawables():
@@ -2143,6 +2147,8 @@
     New API must be flagged with @FlaggedApi: field android.service.notification.NotificationRankingUpdate.PARCELABLE_WRITE_RETURN_VALUE
 UnflaggedApi: android.service.notification.NotificationRankingUpdate#isFdNotNullAndClosed():
     New API must be flagged with @FlaggedApi: method android.service.notification.NotificationRankingUpdate.isFdNotNullAndClosed()
+UnflaggedApi: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy):
+    New API must be flagged with @FlaggedApi: constructor android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy)
 UnflaggedApi: android.telephony.TelephonyManager#HAL_SERVICE_SATELLITE:
     New API must be flagged with @FlaggedApi: field android.telephony.TelephonyManager.HAL_SERVICE_SATELLITE
 UnflaggedApi: android.telephony.ims.feature.MmTelFeature.MmTelCapabilities:
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index f9ab55e..5b354fc 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -23,6 +23,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.NotificationManager.InterruptionFilter;
 import android.content.ComponentName;
 import android.net.Uri;
@@ -35,6 +36,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -111,6 +113,30 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface Type {}
 
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_NAME,
+            FIELD_INTERRUPTION_FILTER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public static final int FIELD_NAME = 1 << 0;
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public static final int FIELD_INTERRUPTION_FILTER = 1 << 1;
+
     private boolean enabled;
     private String name;
     private @InterruptionFilter int interruptionFilter;
@@ -120,12 +146,14 @@
     private long creationTime;
     private ZenPolicy mZenPolicy;
     private ZenDeviceEffects mDeviceEffects;
+    // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
     private boolean mModified = false;
     private String mPkg;
-    private int mType = TYPE_UNKNOWN;
+    private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0;
     private int mIconResId;
     private String mTriggerDescription;
     private boolean mAllowManualInvocation;
+    private @ModifiableField int mUserModifiedFields; // Bitwise representation
 
     /**
      * The maximum string length for any string contained in this automatic zen rule. This pertains
@@ -228,6 +256,7 @@
             mIconResId = source.readInt();
             mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
             mType = source.readInt();
+            mUserModifiedFields = source.readInt();
         }
     }
 
@@ -278,6 +307,8 @@
      * Returns whether this rule's name has been modified by the user.
      * @hide
      */
+    // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once
+    //  FLAG_MODES_API is inlined.
     public boolean isModified() {
         return mModified;
     }
@@ -475,6 +506,32 @@
         return type;
     }
 
+    /**
+     * Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @TestApi
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
+    }
+
+    /**
+     * Returns {@code true} if the {@link AutomaticZenRule} can be updated.
+     * When this returns {@code false}, calls to
+     * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule
+     * will ignore changes to user-configurable fields.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean canUpdate() {
+        // The rule is considered updateable if its bitmask has no user modifications, and
+        // the bitmasks of the policy and device effects have no modification.
+        return mUserModifiedFields == 0
+                && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0)
+                && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0);
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -503,6 +560,7 @@
             dest.writeInt(mIconResId);
             dest.writeString(mTriggerDescription);
             dest.writeInt(mType);
+            dest.writeInt(mUserModifiedFields);
         }
     }
 
@@ -524,12 +582,26 @@
                     .append(",allowManualInvocation=").append(mAllowManualInvocation)
                     .append(",iconResId=").append(mIconResId)
                     .append(",triggerDescription=").append(mTriggerDescription)
-                    .append(",type=").append(mType);
+                    .append(",type=").append(mType)
+                    .append(",userModifiedFields=")
+                    .append(modifiedFieldsToString(mUserModifiedFields));
         }
 
         return sb.append(']').toString();
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private String modifiedFieldsToString(int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_NAME) != 0) {
+            modified.add("FIELD_NAME");
+        }
+        if ((bitmask & FIELD_INTERRUPTION_FILTER) != 0) {
+            modified.add("FIELD_INTERRUPTION_FILTER");
+        }
+        return "{" + String.join(",", modified) + "}";
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (!(o instanceof AutomaticZenRule)) return false;
@@ -551,7 +623,8 @@
                     && other.mAllowManualInvocation == mAllowManualInvocation
                     && other.mIconResId == mIconResId
                     && Objects.equals(other.mTriggerDescription, mTriggerDescription)
-                    && other.mType == mType;
+                    && other.mType == mType
+                    && other.mUserModifiedFields == mUserModifiedFields;
         }
         return finalEquals;
     }
@@ -561,7 +634,8 @@
         if (Flags.modesApi()) {
             return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
                     configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime,
-                    mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
+                    mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType,
+                    mUserModifiedFields);
         }
         return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
                 configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
@@ -630,6 +704,7 @@
         private boolean mAllowManualInvocation;
         private long mCreationTime;
         private String mPkg;
+        private @ModifiableField int mUserModifiedFields;
 
         public Builder(@NonNull AutomaticZenRule rule) {
             mName = rule.getName();
@@ -646,6 +721,7 @@
             mAllowManualInvocation = rule.isManualInvocationAllowed();
             mCreationTime = rule.getCreationTime();
             mPkg = rule.getPackageName();
+            mUserModifiedFields = rule.mUserModifiedFields;
         }
 
         public Builder(@NonNull String name, @NonNull Uri conditionId) {
@@ -772,6 +848,19 @@
             return this;
         }
 
+        /**
+         * Sets the bitmask representing which fields have been user-modified.
+         * This method should not be used outside of tests. The value of userModifiedFields
+         * should be set based on what values are changed when a rule is populated or updated..
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        @TestApi
+        public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+
         public @NonNull AutomaticZenRule build() {
             AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity,
                     mConditionId, mPolicy, mInterruptionFilter, mEnabled);
@@ -782,6 +871,7 @@
             rule.mIconResId = mIconResId;
             rule.mAllowManualInvocation = mAllowManualInvocation;
             rule.setPackageName(mPkg);
+            rule.mUserModifiedFields = mUserModifiedFields;
 
             return rule;
         }
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 30871e9..9fe8af5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -23,7 +23,6 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.app.Activity;
-import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
@@ -37,7 +36,6 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Printer;
 import android.window.OnBackInvokedCallback;
@@ -1790,8 +1788,7 @@
      * @hide
      */
     public boolean isChangeEnabled(long changeId) {
-        return CompatChanges.isChangeEnabled(changeId, applicationInfo.packageName,
-                UserHandle.getUserHandleForUid(applicationInfo.uid));
+        return applicationInfo.isChangeEnabled(changeId);
     }
 
     /** @hide */
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 869c621..f0a8996 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -26,6 +26,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.compat.CompatChanges;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -2645,6 +2646,17 @@
     }
 
     /**
+     * Checks if a changeId is enabled for the current user
+     * @param changeId The changeId to verify
+     * @return True of the changeId is enabled
+     * @hide
+     */
+    public boolean isChangeEnabled(long changeId) {
+        return CompatChanges.isChangeEnabled(changeId, packageName,
+                UserHandle.getUserHandleForUid(uid));
+    }
+
+    /**
      * @return whether the app has requested exemption from the foreground service restrictions.
      * This does not take any affect for now.
      * @hide
diff --git a/core/java/android/content/pm/ModuleInfo.java b/core/java/android/content/pm/ModuleInfo.java
index a1c8747..c6e93bb 100644
--- a/core/java/android/content/pm/ModuleInfo.java
+++ b/core/java/android/content/pm/ModuleInfo.java
@@ -16,7 +16,6 @@
 
 package android.content.pm;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Parcel;
@@ -122,18 +121,15 @@
         return mApexModuleName;
     }
 
-    /** @hide Sets the list of the package name of APK-in-APEX apps in this module. */
+    /** @hide Set the list of the package names of all APK-in-APEX apps in this module. */
     public ModuleInfo setApkInApexPackageNames(@NonNull Collection<String> apkInApexPackageNames) {
         Objects.requireNonNull(apkInApexPackageNames);
         mApkInApexPackageNames = List.copyOf(apkInApexPackageNames);
         return this;
     }
 
-    /**
-     * Gets the list of the package name of all APK-in-APEX apps in the module.
-     */
+    /** @hide Get the list of the package names of all APK-in-APEX apps in the module. */
     @NonNull
-    @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     public Collection<String> getApkInApexPackageNames() {
         if (mApkInApexPackageNames == null) {
             return Collections.emptyList();
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 9e09759..56f69a6 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -450,11 +450,14 @@
          * automatically launched upon the display creation. If unset or set to {@code false}, the
          * display will not host any activities upon creation.</p>
          *
-         * <p>Note: setting to {@code true} requires the display to be trusted. If the display is
-         * not trusted, this property is ignored.</p>
+         * <p>Note: setting to {@code true} requires the display to be trusted and to not mirror
+         * content of other displays. If the display is not trusted, or if it mirrors content of
+         * other displays, this property is ignored.</p>
          *
          * @param isHomeSupported whether home activities are supported on the display
          * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
          * @hide
          */
         @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME)
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 211bdef..144e64f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -108,10 +108,10 @@
 import java.lang.annotation.Target;
 import java.lang.reflect.Field;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -3585,10 +3585,12 @@
                     || applicationInfo.isSignedWithPlatformKey();
         }
 
-        public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
-                List<String> names) {
+        private ArrayMap<String, String> getStringsForPrefixStripPrefix(
+                ContentResolver cr, String prefix, String[] names) {
             String namespace = prefix.substring(0, prefix.length() - 1);
             ArrayMap<String, String> keyValues = new ArrayMap<>();
+            int substringLength = prefix.length();
+
             int currentGeneration = -1;
             boolean needsGenerationTracker = false;
 
@@ -3613,10 +3615,13 @@
                             if (DEBUG) {
                                 Log.i(TAG, "Cache hit for prefix:" + prefix);
                             }
-                            if (!names.isEmpty()) {
+                            if (names.length > 0) {
                                 for (String name : names) {
-                                    if (mValues.containsKey(name)) {
-                                        keyValues.put(name, mValues.get(name));
+                                    String value = mValues.get(name);
+                                    if (value != null) {
+                                        keyValues.put(
+                                                name.substring(substringLength),
+                                                value);
                                     }
                                 }
                             } else {
@@ -3625,7 +3630,10 @@
                                     // Explicitly exclude the prefix as it is only there to
                                     // signal that the prefix has been cached.
                                     if (key.startsWith(prefix) && !key.equals(prefix)) {
-                                        keyValues.put(key, mValues.get(key));
+                                        String value = mValues.valueAt(i);
+                                        keyValues.put(
+                                                key.substring(substringLength),
+                                                value);
                                     }
                                 }
                             }
@@ -3685,14 +3693,22 @@
                 Map<String, String> flagsToValues =
                         (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class);
                 // Only the flags requested by the caller
-                if (!names.isEmpty()) {
-                    for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
-                        if (names.contains(flag.getKey())) {
-                            keyValues.put(flag.getKey(), flag.getValue());
+                if (names.length > 0) {
+                    for (String name : names) {
+                        String value = flagsToValues.get(name);
+                        if (value != null) {
+                            keyValues.put(
+                                    name.substring(substringLength),
+                                    value);
                         }
                     }
                 } else {
-                    keyValues.putAll(flagsToValues);
+                    keyValues.ensureCapacity(keyValues.size() + flagsToValues.size());
+                    for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
+                        keyValues.put(
+                                flag.getKey().substring(substringLength),
+                                flag.getValue());
+                    }
                 }
 
                 synchronized (NameValueCache.this) {
@@ -19675,6 +19691,15 @@
             @Readable
             public static final String WRIST_DETECTION_AUTO_LOCKING_ENABLED =
                     "wear_wrist_detection_auto_locking_enabled";
+
+            /**
+             * Whether consistent notification blocking experience is enabled.
+             *
+             * @hide
+             */
+            @Readable
+            public static final String CONSISTENT_NOTIFICATION_BLOCKING_ENABLED =
+                    "consistent_notification_blocking_enabled";
         }
     }
 
@@ -19835,21 +19860,15 @@
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
         public static Map<String, String> getStrings(@NonNull ContentResolver resolver,
                 @NonNull String namespace, @NonNull List<String> names) {
-            List<String> compositeNames = new ArrayList<>(names.size());
-            for (String name : names) {
-                compositeNames.add(createCompositeName(namespace, name));
+            String[] compositeNames = new String[names.size()];
+            for (int i = 0, size = names.size(); i < size; ++i) {
+                compositeNames[i] = createCompositeName(namespace, names.get(i));
             }
 
             String prefix = createPrefix(namespace);
-            ArrayMap<String, String> rawKeyValues = sNameValueCache.getStringsForPrefix(
+
+            ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix(
                     resolver, prefix, compositeNames);
-            int size = rawKeyValues.size();
-            int substringLength = prefix.length();
-            ArrayMap<String, String> keyValues = new ArrayMap<>(size);
-            for (int i = 0; i < size; ++i) {
-                keyValues.put(rawKeyValues.keyAt(i).substring(substringLength),
-                        rawKeyValues.valueAt(i));
-            }
             return keyValues;
         }
 
@@ -20175,12 +20194,13 @@
         private static String createCompositeName(@NonNull String namespace, @NonNull String name) {
             Preconditions.checkNotNull(namespace);
             Preconditions.checkNotNull(name);
-            return createPrefix(namespace) + name;
+            var sb = new StringBuilder(namespace.length() + 1 + name.length());
+            return sb.append(namespace).append('/').append(name).toString();
         }
 
         private static String createPrefix(@NonNull String namespace) {
             Preconditions.checkNotNull(namespace);
-            return namespace + "/";
+            return namespace + '/';
         }
 
         private static Uri createNamespaceUri(@NonNull String namespace) {
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 0e82b6c..03ebae5 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -17,12 +17,16 @@
 package android.service.notification;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Objects;
 
@@ -33,6 +37,76 @@
 @FlaggedApi(Flags.FLAG_MODES_API)
 public final class ZenDeviceEffects implements Parcelable {
 
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_GRAYSCALE,
+            FIELD_SUPPRESS_AMBIENT_DISPLAY,
+            FIELD_DIM_WALLPAPER,
+            FIELD_NIGHT_MODE,
+            FIELD_DISABLE_AUTO_BRIGHTNESS,
+            FIELD_DISABLE_TAP_TO_WAKE,
+            FIELD_DISABLE_TILT_TO_WAKE,
+            FIELD_DISABLE_TOUCH,
+            FIELD_MINIMIZE_RADIO_USAGE,
+            FIELD_MAXIMIZE_DOZE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_GRAYSCALE = 1 << 0;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 1 << 1;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DIM_WALLPAPER = 1 << 2;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_NIGHT_MODE = 1 << 3;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 1 << 4;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TAP_TO_WAKE = 1 << 5;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TILT_TO_WAKE = 1 << 6;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_DISABLE_TOUCH = 1 << 7;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_MINIMIZE_RADIO_USAGE = 1 << 8;
+    /**
+     * @hide
+     */
+    @TestApi
+    public static final int FIELD_MAXIMIZE_DOZE = 1 << 9;
+
     private final boolean mGrayscale;
     private final boolean mSuppressAmbientDisplay;
     private final boolean mDimWallpaper;
@@ -45,10 +119,13 @@
     private final boolean mMinimizeRadioUsage;
     private final boolean mMaximizeDoze;
 
+    private final @ModifiableField int mUserModifiedFields; // Bitwise representation
+
     private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay,
             boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness,
             boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch,
-            boolean minimizeRadioUsage, boolean maximizeDoze) {
+            boolean minimizeRadioUsage, boolean maximizeDoze,
+            @ModifiableField int userModifiedFields) {
         mGrayscale = grayscale;
         mSuppressAmbientDisplay = suppressAmbientDisplay;
         mDimWallpaper = dimWallpaper;
@@ -59,6 +136,7 @@
         mDisableTouch = disableTouch;
         mMinimizeRadioUsage = minimizeRadioUsage;
         mMaximizeDoze = maximizeDoze;
+        mUserModifiedFields = userModifiedFields;
     }
 
     @Override
@@ -75,14 +153,15 @@
                 && this.mDisableTiltToWake == that.mDisableTiltToWake
                 && this.mDisableTouch == that.mDisableTouch
                 && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage
-                && this.mMaximizeDoze == that.mMaximizeDoze;
+                && this.mMaximizeDoze == that.mMaximizeDoze
+                && this.mUserModifiedFields == that.mUserModifiedFields;
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode,
                 mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch,
-                mMinimizeRadioUsage, mMaximizeDoze);
+                mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields);
     }
 
     @Override
@@ -98,7 +177,43 @@
         if (mDisableTouch) effects.add("disableTouch");
         if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage");
         if (mMaximizeDoze) effects.add("maximizeDoze");
-        return "[" + String.join(", ", effects) + "]";
+        return "[" + String.join(", ", effects) + "]"
+                + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields);
+    }
+
+    private String modifiedFieldsToString(int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_GRAYSCALE) != 0) {
+            modified.add("FIELD_GRAYSCALE");
+        }
+        if ((bitmask & FIELD_SUPPRESS_AMBIENT_DISPLAY) != 0) {
+            modified.add("FIELD_SUPPRESS_AMBIENT_DISPLAY");
+        }
+        if ((bitmask & FIELD_DIM_WALLPAPER) != 0) {
+            modified.add("FIELD_DIM_WALLPAPER");
+        }
+        if ((bitmask & FIELD_NIGHT_MODE) != 0) {
+            modified.add("FIELD_NIGHT_MODE");
+        }
+        if ((bitmask & FIELD_DISABLE_AUTO_BRIGHTNESS) != 0) {
+            modified.add("FIELD_DISABLE_AUTO_BRIGHTNESS");
+        }
+        if ((bitmask & FIELD_DISABLE_TAP_TO_WAKE) != 0) {
+            modified.add("FIELD_DISABLE_TAP_TO_WAKE");
+        }
+        if ((bitmask & FIELD_DISABLE_TILT_TO_WAKE) != 0) {
+            modified.add("FIELD_DISABLE_TILT_TO_WAKE");
+        }
+        if ((bitmask & FIELD_DISABLE_TOUCH) != 0) {
+            modified.add("FIELD_DISABLE_TOUCH");
+        }
+        if ((bitmask & FIELD_MINIMIZE_RADIO_USAGE) != 0) {
+            modified.add("FIELD_MINIMIZE_RADIO_USAGE");
+        }
+        if ((bitmask & FIELD_MAXIMIZE_DOZE) != 0) {
+            modified.add("FIELD_MAXIMIZE_DOZE");
+        }
+        return "{" + String.join(",", modified) + "}";
     }
 
     /**
@@ -194,9 +309,10 @@
     public static final Creator<ZenDeviceEffects> CREATOR = new Creator<ZenDeviceEffects>() {
         @Override
         public ZenDeviceEffects createFromParcel(Parcel in) {
-            return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(),
+            return new ZenDeviceEffects(in.readBoolean(),
                     in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
-                    in.readBoolean(), in.readBoolean(), in.readBoolean());
+                    in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
+                    in.readBoolean(), in.readInt());
         }
 
         @Override
@@ -205,6 +321,16 @@
         }
     };
 
+    /**
+     * Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @TestApi
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -222,6 +348,7 @@
         dest.writeBoolean(mDisableTouch);
         dest.writeBoolean(mMinimizeRadioUsage);
         dest.writeBoolean(mMaximizeDoze);
+        dest.writeInt(mUserModifiedFields);
     }
 
     /** Builder class for {@link ZenDeviceEffects} objects. */
@@ -238,6 +365,7 @@
         private boolean mDisableTouch;
         private boolean mMinimizeRadioUsage;
         private boolean mMaximizeDoze;
+        private @ModifiableField int mUserModifiedFields;
 
         /**
          * Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled).
@@ -260,6 +388,7 @@
             mDisableTouch = zenDeviceEffects.shouldDisableTouch();
             mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage();
             mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze();
+            mUserModifiedFields = zenDeviceEffects.mUserModifiedFields;
         }
 
         /**
@@ -381,12 +510,24 @@
             return this;
         }
 
+        /**
+         * Sets the bitmask representing which fields are user modified. See the FIELD_ constants.
+         * @hide
+         */
+        @TestApi
+        @NonNull
+        public Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+
         /** Builds a {@link ZenDeviceEffects} object based on the builder's state. */
         @NonNull
         public ZenDeviceEffects build() {
-            return new ZenDeviceEffects(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper,
-                    mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake,
-                    mDisableTouch, mMinimizeRadioUsage, mMaximizeDoze);
+            return new ZenDeviceEffects(mGrayscale,
+                    mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness,
+                    mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage,
+                    mMaximizeDoze, mUserModifiedFields);
         }
     }
 }
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index fcdc5fe..45a0c20 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -205,6 +205,7 @@
     private static final String ALLOW_ATT_CONV = "convos";
     private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
     private static final String ALLOW_ATT_CHANNELS = "channels";
+    private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields";
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
     private static final String STATE_TAG = "state";
@@ -247,6 +248,7 @@
     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_USER_MODIFIED_FIELDS = "userModifiedFields";
     private static final String RULE_ATT_ICON = "rule_icon";
     private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
 
@@ -261,6 +263,7 @@
     private static final String DEVICE_EFFECT_DISABLE_TOUCH = "zdeDisableTouch";
     private static final String DEVICE_EFFECT_MINIMIZE_RADIO_USAGE = "zdeMinimizeRadioUsage";
     private static final String DEVICE_EFFECT_MAXIMIZE_DOZE = "zdeMaximizeDoze";
+    private static final String DEVICE_EFFECT_USER_MODIFIED_FIELDS = "zdeUserModifiedFields";
 
     @UnsupportedAppUsage
     public boolean allowAlarms = DEFAULT_ALLOW_ALARMS;
@@ -748,6 +751,7 @@
             rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON);
             rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
             rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
+            rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
         }
         return rt;
     }
@@ -794,6 +798,7 @@
                 out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
             }
             out.attributeInt(null, RULE_ATT_TYPE, rule.type);
+            out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
         }
     }
 
@@ -856,6 +861,7 @@
                 builder.allowChannels(channels);
                 policySet = true;
             }
+            builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0));
         }
 
         if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
@@ -968,6 +974,7 @@
 
         if (Flags.modesApi()) {
             writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out);
+            out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields());
         }
     }
 
@@ -993,6 +1000,7 @@
         }
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
     @Nullable
     private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) {
         ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
@@ -1012,11 +1020,13 @@
                 .setShouldMinimizeRadioUsage(
                         safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false))
                 .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false))
+                .setUserModifiedFields(safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0))
                 .build();
 
         return deviceEffects.hasEffects() ? deviceEffects : null;
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
     private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects,
             TypedXmlSerializer out) throws IOException {
         writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE,
@@ -1035,6 +1045,8 @@
         writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE,
                 deviceEffects.shouldMinimizeRadioUsage());
         writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze());
+        out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
+                deviceEffects.getUserModifiedFields());
     }
 
     private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value)
@@ -1985,6 +1997,7 @@
         public String triggerDescription;
         public String iconResName;
         public boolean allowManualInvocation;
+        public int userModifiedFields;
 
         public ZenRule() { }
 
@@ -2017,9 +2030,22 @@
                 iconResName = source.readString();
                 triggerDescription = source.readString();
                 type = source.readInt();
+                userModifiedFields = source.readInt();
             }
         }
 
+        /**
+         * @see AutomaticZenRule#canUpdate()
+         */
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        public boolean canBeUpdatedByApp() {
+            // The rule is considered updateable if its bitmask has no user modifications, and
+            // the bitmasks of the policy and device effects have no modification.
+            return userModifiedFields == 0
+                    && (zenPolicy == null || zenPolicy.getUserModifiedFields() == 0)
+                    && (zenDeviceEffects == null || zenDeviceEffects.getUserModifiedFields() == 0);
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -2064,6 +2090,7 @@
                 dest.writeString(iconResName);
                 dest.writeString(triggerDescription);
                 dest.writeInt(type);
+                dest.writeInt(userModifiedFields);
             }
         }
 
@@ -2092,7 +2119,8 @@
                         .append(",allowManualInvocation=").append(allowManualInvocation)
                         .append(",iconResName=").append(iconResName)
                         .append(",triggerDescription=").append(triggerDescription)
-                        .append(",type=").append(type);
+                        .append(",type=").append(type)
+                        .append(",userModifiedFields=").append(userModifiedFields);
             }
 
             return sb.append(']').toString();
@@ -2151,7 +2179,8 @@
                         && other.allowManualInvocation == allowManualInvocation
                         && Objects.equals(other.iconResName, iconResName)
                         && Objects.equals(other.triggerDescription, triggerDescription)
-                        && other.type == type;
+                        && other.type == type
+                        && other.userModifiedFields == userModifiedFields;
             }
 
             return finalEquals;
@@ -2163,7 +2192,7 @@
                 return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                         component, configurationActivity, pkg, id, enabler, zenPolicy,
                         zenDeviceEffects, modified, allowManualInvocation, iconResName,
-                        triggerDescription, type);
+                        triggerDescription, type, userModifiedFields);
             }
             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 d87e758..8902368 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -467,6 +467,7 @@
         public static final String FIELD_ICON_RES = "iconResName";
         public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
         public static final String FIELD_TYPE = "type";
+        public static final String FIELD_USER_MODIFIED_FIELDS = "userModifiedFields";
         // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
 
         // Special field to track whether this rule became active or inactive
@@ -562,6 +563,10 @@
                 if (!Objects.equals(from.iconResName, to.iconResName)) {
                     addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
                 }
+                if (from.userModifiedFields != to.userModifiedFields) {
+                    addField(FIELD_USER_MODIFIED_FIELDS,
+                            new FieldDiff<>(from.userModifiedFields, to.userModifiedFields));
+                }
             }
         }
 
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 3c1a279..8477eb7 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -20,6 +20,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -32,6 +34,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -41,12 +44,148 @@
  * a device is in Do Not Disturb mode.
  */
 public final class ZenPolicy implements Parcelable {
-    private ArrayList<Integer> mPriorityCategories;
-    private ArrayList<Integer> mVisualEffects;
+
+    /** Used to track which rule variables have been modified by the user.
+     * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FIELD_" }, value = {
+            FIELD_MESSAGES,
+            FIELD_CALLS,
+            FIELD_CONVERSATIONS,
+            FIELD_ALLOW_CHANNELS,
+            FIELD_PRIORITY_CATEGORY_REMINDERS,
+            FIELD_PRIORITY_CATEGORY_EVENTS,
+            FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS,
+            FIELD_PRIORITY_CATEGORY_ALARMS,
+            FIELD_PRIORITY_CATEGORY_MEDIA,
+            FIELD_PRIORITY_CATEGORY_SYSTEM,
+            FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT,
+            FIELD_VISUAL_EFFECT_LIGHTS,
+            FIELD_VISUAL_EFFECT_PEEK,
+            FIELD_VISUAL_EFFECT_STATUS_BAR,
+            FIELD_VISUAL_EFFECT_BADGE,
+            FIELD_VISUAL_EFFECT_AMBIENT,
+            FIELD_VISUAL_EFFECT_NOTIFICATION_LIST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModifiableField {}
+
+    /**
+     * Covers modifications to MESSAGE_SENDERS and PRIORITY_CATEGORY_MESSAGES, which are set at
+     * the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_MESSAGES = 1 << 0;
+    /**
+     * Covers modifications to CALL_SENDERS and PRIORITY_CATEGORY_CALLS, which are set at
+     * the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_CALLS = 1 << 1;
+    /**
+     * Covers modifications to CONVERSATION_SENDERS and PRIORITY_CATEGORY_CONVERSATIONS, which are
+     * set at the same time.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_CONVERSATIONS = 1 << 2;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_ALLOW_CHANNELS = 1 << 3;
+    /**
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_REMINDERS = 1 << 4;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15;
+    /**
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16;
+
+    private List<Integer> mPriorityCategories;
+    private List<Integer> mVisualEffects;
     private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
     private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
     private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
     private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET;
+    private final @ModifiableField int mUserModifiedFields; // Bitwise representation
 
     /** @hide */
     @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = {
@@ -249,6 +388,22 @@
     public ZenPolicy() {
         mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0));
         mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0));
+        mUserModifiedFields = 0;
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects,
+                     @PeopleType int priorityMessages, @PeopleType int priorityCalls,
+                     @ConversationSenders int conversationSenders, @ChannelType int allowChannels,
+                     @ModifiableField int userModifiedFields) {
+        mPriorityCategories = priorityCategories;
+        mVisualEffects = visualEffects;
+        mPriorityMessages = priorityMessages;
+        mPriorityCalls = priorityCalls;
+        mConversationSenders = conversationSenders;
+        mAllowChannels = allowChannels;
+        mUserModifiedFields = userModifiedFields;
     }
 
     /**
@@ -473,6 +628,8 @@
      * is not set, it is (@link STATE_UNSET} and will not change the current set policy.
      */
     public static final class Builder {
+        private @ModifiableField int mUserModifiedFields;
+
         private ZenPolicy mZenPolicy;
 
         public Builder() {
@@ -482,9 +639,14 @@
         /**
          * @hide
          */
-        public Builder(ZenPolicy policy) {
+        @SuppressLint("UnflaggedApi")
+        @TestApi
+        public Builder(@Nullable ZenPolicy policy) {
             if (policy != null) {
                 mZenPolicy = policy.copy();
+                if (Flags.modesApi()) {
+                    mUserModifiedFields = policy.mUserModifiedFields;
+                }
             } else {
                 mZenPolicy = new ZenPolicy();
             }
@@ -494,7 +656,15 @@
          * Builds the current ZenPolicy.
          */
         public @NonNull ZenPolicy build() {
-            return mZenPolicy.copy();
+            if (Flags.modesApi()) {
+                return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories),
+                        new ArrayList<Integer>(mZenPolicy.mVisualEffects),
+                        mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
+                        mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels,
+                        mUserModifiedFields);
+            } else {
+                return mZenPolicy.copy();
+            }
         }
 
         /**
@@ -850,6 +1020,28 @@
             mZenPolicy.mAllowChannels = channelType;
             return this;
         }
+
+        /**
+         * Sets the user modified fields bitmask.
+         * @hide
+         */
+        @TestApi
+        @FlaggedApi(Flags.FLAG_MODES_API)
+        public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
+            mUserModifiedFields = userModifiedFields;
+            return this;
+        }
+    }
+
+    /**
+     Gets the bitmask representing which fields are user modified. Bits are set using
+     * {@link ModifiableField}.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @ModifiableField int getUserModifiedFields() {
+        return mUserModifiedFields;
     }
 
     @Override
@@ -861,39 +1053,49 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeList(mPriorityCategories);
         dest.writeList(mVisualEffects);
-        dest.writeInt(mPriorityCalls);
         dest.writeInt(mPriorityMessages);
+        dest.writeInt(mPriorityCalls);
         dest.writeInt(mConversationSenders);
         if (Flags.modesApi()) {
             dest.writeInt(mAllowChannels);
+            dest.writeInt(mUserModifiedFields);
         }
     }
 
-    public static final @android.annotation.NonNull Parcelable.Creator<ZenPolicy> CREATOR =
-            new Parcelable.Creator<ZenPolicy>() {
-        @Override
-        public ZenPolicy createFromParcel(Parcel source) {
-            ZenPolicy policy = new ZenPolicy();
-            policy.mPriorityCategories = trimList(
-                    source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
-                    NUM_PRIORITY_CATEGORIES);
-            policy.mVisualEffects = trimList(
-                    source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
-                    NUM_VISUAL_EFFECTS);
-            policy.mPriorityCalls = source.readInt();
-            policy.mPriorityMessages = source.readInt();
-            policy.mConversationSenders = source.readInt();
-            if (Flags.modesApi()) {
-                policy.mAllowChannels = source.readInt();
-            }
-            return policy;
-        }
+    public static final @NonNull Creator<ZenPolicy> CREATOR =
+            new Creator<ZenPolicy>() {
+                @Override
+                public ZenPolicy createFromParcel(Parcel source) {
+                    ZenPolicy policy;
+                    if (Flags.modesApi()) {
+                        policy = new ZenPolicy(
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_PRIORITY_CATEGORIES),
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_VISUAL_EFFECTS),
+                                source.readInt(), source.readInt(), source.readInt(),
+                                source.readInt(), source.readInt()
+                        );
+                    } else {
+                        policy = new ZenPolicy();
+                        policy.mPriorityCategories =
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_PRIORITY_CATEGORIES);
+                        policy.mVisualEffects =
+                                trimList(source.readArrayList(Integer.class.getClassLoader(),
+                                        Integer.class), NUM_VISUAL_EFFECTS);
+                        policy.mPriorityMessages = source.readInt();
+                        policy.mPriorityCalls = source.readInt();
+                        policy.mConversationSenders = source.readInt();
+                    }
+                    return policy;
+                }
 
-        @Override
-        public ZenPolicy[] newArray(int size) {
-            return new ZenPolicy[size];
-        }
-    };
+                @Override
+                public ZenPolicy[] newArray(int size) {
+                    return new ZenPolicy[size];
+                }
+            };
 
     @Override
     public String toString() {
@@ -907,10 +1109,69 @@
                         conversationTypeToString(mConversationSenders));
         if (Flags.modesApi()) {
             sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels));
+            sb.append(", userModifiedFields=")
+                    .append(modifiedFieldsToString(mUserModifiedFields));
         }
         return sb.append('}').toString();
     }
 
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private String modifiedFieldsToString(@ModifiableField int bitmask) {
+        ArrayList<String> modified = new ArrayList<>();
+        if ((bitmask & FIELD_MESSAGES) != 0) {
+            modified.add("FIELD_MESSAGES");
+        }
+        if ((bitmask & FIELD_CALLS) != 0) {
+            modified.add("FIELD_CALLS");
+        }
+        if ((bitmask & FIELD_CONVERSATIONS) != 0) {
+            modified.add("FIELD_CONVERSATIONS");
+        }
+        if ((bitmask & FIELD_ALLOW_CHANNELS) != 0) {
+            modified.add("FIELD_ALLOW_CHANNELS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_REMINDERS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_REMINDERS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_EVENTS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_EVENTS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_ALARMS) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_ALARMS");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_MEDIA) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_MEDIA");
+        }
+        if ((bitmask & FIELD_PRIORITY_CATEGORY_SYSTEM) != 0) {
+            modified.add("FIELD_PRIORITY_CATEGORY_SYSTEM");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_LIGHTS) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_LIGHTS");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_PEEK) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_PEEK");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_STATUS_BAR) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_STATUS_BAR");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_BADGE) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_BADGE");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_AMBIENT) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_AMBIENT");
+        }
+        if ((bitmask & FIELD_VISUAL_EFFECT_NOTIFICATION_LIST) != 0) {
+            modified.add("FIELD_VISUAL_EFFECT_NOTIFICATION_LIST");
+        }
+        return "{" + String.join(",", modified) + "}";
+    }
+
     // Returns a list containing the first maxLength elements of the input list if the list is
     // longer than that size. For the lists in ZenPolicy, this should not happen unless the input
     // is corrupt.
@@ -1066,7 +1327,8 @@
                 && other.mPriorityMessages == mPriorityMessages
                 && other.mConversationSenders == mConversationSenders;
         if (Flags.modesApi()) {
-            return eq && other.mAllowChannels == mAllowChannels;
+            return eq && other.mAllowChannels == mAllowChannels
+                    && other.mUserModifiedFields == mUserModifiedFields;
         }
         return eq;
     }
@@ -1075,13 +1337,13 @@
     public int hashCode() {
         if (Flags.modesApi()) {
             return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
-                    mPriorityMessages, mConversationSenders, mAllowChannels);
+                    mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields);
         }
         return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
                 mConversationSenders);
     }
 
-    private @ZenPolicy.State int getZenPolicyPriorityCategoryState(@PriorityCategory int
+    private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int
             category) {
         switch (category) {
             case PRIORITY_CATEGORY_REMINDERS:
@@ -1106,7 +1368,7 @@
         return -1;
     }
 
-    private @ZenPolicy.State int getZenPolicyVisualEffectState(@VisualEffect int effect) {
+    private @State int getZenPolicyVisualEffectState(@VisualEffect int effect) {
         switch (effect) {
             case VISUAL_EFFECT_FULL_SCREEN_INTENT:
                 return getVisualEffectFullScreenIntent();
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8529b4e..350876c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -6943,7 +6943,11 @@
         }
 
         private int doOnBackKeyEvent(KeyEvent keyEvent) {
-            OnBackInvokedCallback topCallback = getOnBackInvokedDispatcher().getTopCallback();
+            WindowOnBackInvokedDispatcher dispatcher = getOnBackInvokedDispatcher();
+            OnBackInvokedCallback topCallback = dispatcher.getTopCallback();
+            if (dispatcher.isDispatching()) {
+                return FINISH_NOT_HANDLED;
+            }
             if (topCallback instanceof OnBackAnimationCallback) {
                 final OnBackAnimationCallback animationCallback =
                         (OnBackAnimationCallback) topCallback;
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index feccc6b..3bc02a6 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -354,7 +354,11 @@
      * @hide
      */
     public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {
-        forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
+        // Skip this call if we are in system_server, as the system code should not use this
+        // deprecated instance.
+        if (!ActivityThread.isSystem()) {
+            forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
+        }
     }
 
     private static final Object sLock = new Object();
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 6a8ca33..86804c6 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -174,6 +174,21 @@
         }
     }
 
+    /**
+     * Indicates if the dispatcher is actively dispatching to a callback.
+     */
+    public boolean isDispatching() {
+        return mIsDispatching;
+    }
+
+    private void onStartDispatching() {
+        mIsDispatching = true;
+    }
+
+    private void onStopDispatching() {
+        mIsDispatching = false;
+    }
+
     private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) {
         boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
         if (isInProgress && callback instanceof OnBackAnimationCallback) {
@@ -231,7 +246,7 @@
                                     .ImeOnBackInvokedCallback
                                 ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback)
                                         callback).getIOnBackInvokedCallback()
-                                : new OnBackInvokedCallbackWrapper(callback);
+                                : new OnBackInvokedCallbackWrapper(callback, this);
                 callbackInfo = new OnBackInvokedCallbackInfo(
                         iCallback,
                         priority,
@@ -258,6 +273,7 @@
 
     @NonNull
     private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+    private boolean mIsDispatching = false;
 
     /**
      * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it
@@ -317,18 +333,33 @@
             }
         }
         final CallbackRef mCallbackRef;
+        /**
+         * The dispatcher this callback is registered with.
+         * This can be null for callbacks on {@link ImeOnBackInvokedDispatcher} because they are
+         * forwarded and registered on the app's {@link WindowOnBackInvokedDispatcher}. */
+        @Nullable
+        private final WindowOnBackInvokedDispatcher mDispatcher;
 
-        OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
+        OnBackInvokedCallbackWrapper(
+                @NonNull OnBackInvokedCallback callback,
+                WindowOnBackInvokedDispatcher dispatcher) {
             mCallbackRef = new CallbackRef(callback, true /* useWeakRef */);
+            mDispatcher = dispatcher;
         }
 
-        OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) {
+        OnBackInvokedCallbackWrapper(
+                @NonNull OnBackInvokedCallback callback,
+                boolean useWeakRef) {
             mCallbackRef = new CallbackRef(callback, useWeakRef);
+            mDispatcher = null;
         }
 
         @Override
         public void onBackStarted(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStartDispatching();
+                }
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     mProgressAnimator.onBackStarted(backEvent, event ->
@@ -353,6 +384,9 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStopDispatching();
+                }
                 mProgressAnimator.onBackCancelled(() -> {
                     final OnBackAnimationCallback callback = getBackAnimationCallback();
                     if (callback != null) {
@@ -365,6 +399,9 @@
         @Override
         public void onBackInvoked() throws RemoteException {
             Handler.getMain().post(() -> {
+                if (mDispatcher != null) {
+                    mDispatcher.onStopDispatching();
+                }
                 boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
                 mProgressAnimator.reset();
                 final OnBackInvokedCallback callback = mCallbackRef.get();
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 54fdcc6..4e3b64c 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -47,6 +47,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources.Theme;
@@ -389,10 +390,7 @@
         mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
         mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
-        mEdgeToEdgeEnforced =
-                context.getApplicationInfo().targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
-                        || (CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
-                                && Flags.enforceEdgeToEdge());
+        mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */);
         if (mEdgeToEdgeEnforced) {
             getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
             mDecorFitsSystemWindows = false;
@@ -433,6 +431,22 @@
         mActivityConfigCallback = activityConfigCallback;
     }
 
+    /**
+     * Returns whether the given application is enforced to go edge-to-edge.
+     *
+     * @param info The application to query.
+     * @param local Whether this is called from the process of the given application.
+     * @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}.
+     */
+    public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) {
+        return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
+                || (Flags.enforceEdgeToEdge() && (local
+                        // Calling this doesn't require a permission.
+                        ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
+                        // Calling this requires permissions.
+                        : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)));
+    }
+
     @Override
     public final void setContainer(Window container) {
         super.setContainer(container);
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 8d80af4..5346454 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -212,6 +212,36 @@
     <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool>
     <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" />
 
+    <!-- List of country codes where oem-enabled satellite services are either allowed or disallowed
+         by the device. Each country code is a lowercase 2 character ISO-3166-1 alpha-2.
+         -->
+    <string-array name="config_oem_enabled_satellite_country_codes">
+    </string-array>
+    <java-symbol type="array" name="config_oem_enabled_satellite_country_codes" />
+
+    <!-- The file storing S2-cell-based satellite access restriction of the countries defined by
+         config_oem_enabled_satellite_countries. -->
+    <string name="config_oem_enabled_satellite_s2cell_file"></string>
+    <java-symbol type="string" name="config_oem_enabled_satellite_s2cell_file" />
+
+    <!-- Whether to treat the countries defined by the config_oem_enabled_satellite_countries
+         as satellite-allowed areas. The default true value means the countries defined by
+         config_oem_enabled_satellite_countries will be treated as satellite-allowed areas.
+         -->
+    <bool name="config_oem_enabled_satellite_access_allow">true</bool>
+    <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" />
+
+    <!-- The time duration in seconds which is used to decide whether the Location returned from
+         LocationManager#getLastKnownLocation is fresh.
+
+         The Location is considered fresh if the duration from the Location's elapsed real time to
+         the current elapsed real time is less than this config. If the Location is considered
+         fresh, it will be used as the current location by Telephony to decide whether satellite
+         services should be allowed.
+         -->
+    <integer name="config_oem_enabled_satellite_location_fresh_duration">600</integer>
+    <java-symbol type="integer" name="config_oem_enabled_satellite_location_fresh_duration" />
+
     <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
          will not perform handover if the target transport is out of service, or VoPS not
          supported. The network will be torn down on the source transport, and will be
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 1925588..9d85b65 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.fail;
 
@@ -26,6 +28,8 @@
 import android.os.Parcel;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenPolicy;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -226,4 +230,66 @@
 
         assertThrows(IllegalArgumentException.class, () -> builder.setType(100));
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_nullPolicyAndDeviceEffects() {
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(null)
+                .setDeviceEffects(null)
+                .build();
+
+        assertThat(rule.canUpdate()).isTrue();
+
+        rule = builder.setUserModifiedFields(1).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_policyModified() {
+        ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
+        ZenPolicy policy = policyBuilder.build();
+
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(policy)
+                .setDeviceEffects(null).build();
+
+        // Newly created ZenPolicy is not user modified.
+        assertThat(policy.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.canUpdate()).isTrue();
+
+        policy = policyBuilder.setUserModifiedFields(1).build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(1);
+        rule = builder.setZenPolicy(policy).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testCanUpdate_deviceEffectsModified() {
+        ZenDeviceEffects.Builder deviceEffectsBuilder =
+                new ZenDeviceEffects.Builder().setUserModifiedFields(0);
+        ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+
+        AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
+                Uri.parse("uri://short"));
+        AutomaticZenRule rule = builder.setUserModifiedFields(0)
+                .setZenPolicy(null)
+                .setDeviceEffects(deviceEffects).build();
+
+        // Newly created ZenDeviceEffects is not user modified.
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.canUpdate()).isTrue();
+
+        deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
+        rule = builder.setDeviceEffects(deviceEffects).build();
+        assertThat(rule.canUpdate()).isFalse();
+    }
 }
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 43683ff..ce2543a 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -56,6 +56,7 @@
         <permission name="android.permission.REAL_GET_TASKS"/>
         <permission name="android.permission.REQUEST_NETWORK_SCORES"/>
         <permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/>
+        <permission name="android.permission.SATELLITE_COMMUNICATION"/>
         <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/>
         <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
         <permission name="android.permission.START_ACTIVITY_AS_CALLER"/>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b9efe65..a1ea2b8 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -134,6 +134,7 @@
         <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
         <permission name="android.permission.DUMP"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.LOCATION_BYPASS"/>
         <permission name="android.permission.LOCAL_MAC_ADDRESS"/>
         <permission name="android.permission.MANAGE_USERS"/>
         <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS" />
@@ -149,6 +150,7 @@
         <permission name="android.permission.REGISTER_CALL_PROVIDER"/>
         <permission name="android.permission.REGISTER_SIM_SUBSCRIPTION"/>
         <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/>
+        <permission name="android.permission.SATELLITE_COMMUNICATION"/>
         <permission name="android.permission.SEND_RESPOND_VIA_MESSAGE"/>
         <permission name="android.permission.SHUTDOWN"/>
         <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 592f9a5..80afb16d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -382,9 +382,13 @@
         if (splitAttributes == null) {
             return TaskFragmentAnimationParams.DEFAULT;
         }
-        return new TaskFragmentAnimationParams.Builder()
-                // TODO(b/263047900): Update extensions API.
-                // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
-                .build();
+        final AnimationBackground animationBackground = splitAttributes.getAnimationBackground();
+        if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) {
+            return new TaskFragmentAnimationParams.Builder()
+                    .setAnimationBackgroundColor(colorBackground.getColor())
+                    .build();
+        } else {
+            return TaskFragmentAnimationParams.DEFAULT;
+        }
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 6f356fa..8b7fd10 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -893,8 +893,7 @@
         return new SplitAttributes.Builder()
                 .setSplitType(splitTypeToUpdate)
                 .setLayoutDirection(splitAttributes.getLayoutDirection())
-                // TODO(b/263047900): Update extensions API.
-                // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
+                .setAnimationBackground(splitAttributes.getAnimationBackground())
                 .build();
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
index 60beb0b..f471af0 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
@@ -25,6 +25,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.window.extensions.embedding.AnimationBackground;
 import androidx.window.extensions.embedding.SplitAttributes;
 
 import org.junit.Before;
@@ -70,7 +71,7 @@
                 .isEqualTo(SplitAttributes.LayoutDirection.LOCALE);
         assertThat(splitAttributes.getSplitType())
                 .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f));
-        // TODO(b/263047900): Update extensions API.
-        // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0);
+        assertThat(splitAttributes.getAnimationBackground())
+                .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index ac75c73..06210ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
@@ -330,6 +331,9 @@
             if (!animation.hasExtension()) {
                 continue;
             }
+            if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)) {
+                continue;
+            }
             final TransitionInfo.Change change = adapter.mChange;
             if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
                 // Need to screenshot after startTransaction is applied otherwise activity
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index a498236..81d9638 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -403,8 +403,8 @@
         mCurrentTracker.updateStartLocation();
         // Dispatch onBackStarted, only to app callbacks.
         // System callbacks will receive onBackStarted when the remote animation starts.
-        if (!shouldDispatchToAnimator()) {
-            tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+        if (!shouldDispatchToAnimator() && mActiveCallback != null) {
+            tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
         }
     }
 
@@ -507,7 +507,7 @@
             mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
             // App is handling back animation. Cancel system animation latency tracking.
             cancelLatencyTracking();
-            tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+            tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
         }
     }
 
@@ -551,14 +551,24 @@
                 && mBackNavigationInfo.isPrepareRemoteAnimation();
     }
 
-    private void tryDispatchOnBackStarted(IOnBackInvokedCallback callback,
+    private void tryDispatchAppOnBackStarted(
+            IOnBackInvokedCallback callback,
             BackMotionEvent backEvent) {
-        if (callback == null || mOnBackStartDispatched) {
+        if (mOnBackStartDispatched && callback != null) {
+            return;
+        }
+        dispatchOnBackStarted(callback, backEvent);
+        mOnBackStartDispatched = true;
+    }
+
+    private void dispatchOnBackStarted(
+            IOnBackInvokedCallback callback,
+            BackMotionEvent backEvent) {
+        if (callback == null) {
             return;
         }
         try {
             callback.onBackStarted(backEvent);
-            mOnBackStartDispatched = true;
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackStarted error: ", e);
         }
@@ -940,9 +950,17 @@
 
                                     if (apps.length >= 1) {
                                         mCurrentTracker.updateStartLocation();
-                                        tryDispatchOnBackStarted(
-                                                mActiveCallback,
-                                                mCurrentTracker.createStartEvent(apps[0]));
+                                        BackMotionEvent startEvent =
+                                                mCurrentTracker.createStartEvent(apps[0]);
+                                        // {@code mActiveCallback} is the callback from
+                                        // the BackAnimationRunners and not a real app-side
+                                        // callback. We also dispatch to the app-side callback
+                                        // (which should be a system callback with PRIORITY_SYSTEM)
+                                        // to keep consistent with app registered callbacks.
+                                        dispatchOnBackStarted(mActiveCallback, startEvent);
+                                        tryDispatchAppOnBackStarted(
+                                                mBackNavigationInfo.getOnBackInvokedCallback(),
+                                                startEvent);
                                     }
 
                                     // Dispatch the first progress after animation start for
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
index 0693543..662f325 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.common.split;
 
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
@@ -24,19 +24,27 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.PendingIntent;
-import android.content.Context;
+import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 
+import java.util.Arrays;
+import java.util.List;
+
 /** Helper utility class for split screen components to use. */
 public class SplitScreenUtils {
     /** Reverse the split position. */
@@ -135,4 +143,28 @@
             return isLandscape;
         }
     }
+
+    /** Returns the component from a PendingIntent */
+    @Nullable
+    public static ComponentName getComponent(@Nullable PendingIntent pendingIntent) {
+        if (pendingIntent == null || pendingIntent.getIntent() == null) {
+            return null;
+        }
+        return pendingIntent.getIntent().getComponent();
+    }
+
+    /** Returns the component from a shortcut */
+    @Nullable
+    public static ComponentName getShortcutComponent(@NonNull String packageName, String shortcutId,
+            @NonNull UserHandle user, @NonNull LauncherApps launcherApps) {
+        LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery();
+        query.setPackage(packageName);
+        query.setShortcutIds(Arrays.asList(shortcutId));
+        query.setQueryFlags(FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED);
+        List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(query, user);
+        ShortcutInfo info = shortcuts != null && shortcuts.size() > 0
+                ? shortcuts.get(0)
+                : null;
+        return info != null ? info.getActivity() : null;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 3906599..8b3de62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -52,9 +52,10 @@
      * @param componentName ComponentName represents the Activity
      * @param destinationBounds the destination bounds the PiP window lands into
      * @param overlay an optional overlay to fade out after entering PiP
+     * @param appBounds the bounds used to set the buffer size of the optional content overlay
      */
     oneway void stopSwipePipToHome(int taskId, in ComponentName componentName,
-            in Rect destinationBounds, in SurfaceControl overlay) = 2;
+            in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2;
 
     /**
      * Notifies the swiping Activity to PiP onto home transition is aborted
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 3635165..a9a3f78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -334,6 +334,16 @@
     @Nullable
     SurfaceControl mPipOverlay;
 
+    /**
+     * The app bounds used for the buffer size of the
+     * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}.
+     *
+     * Note that this is empty if the overlay is removed or if it's some other type of overlay
+     * defined in {@link PipContentOverlay}.
+     */
+    @NonNull
+    final Rect mAppBounds = new Rect();
+
     public PipTaskOrganizer(Context context,
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
@@ -464,15 +474,15 @@
      * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
      */
     public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay) {
+            SurfaceControl overlay, Rect appBounds) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "stopSwipePipToHome: %s, state=%s", componentName, mPipTransitionState);
+                "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState);
         // do nothing if there is no startSwipePipToHome being called before
         if (!mPipTransitionState.getInSwipePipToHomeTransition()) {
             return;
         }
         mPipBoundsState.setBounds(destinationBounds);
-        mPipOverlay = overlay;
+        setContentOverlay(overlay, appBounds);
         if (ENABLE_SHELL_TRANSITIONS && overlay != null) {
             // With Shell transition, the overlay was attached to the remote transition leash, which
             // will be removed when the current transition is finished, so we need to reparent it
@@ -1888,7 +1898,7 @@
                         "%s: trying to remove overlay (%s) which is not local reference (%s)",
                         TAG, surface, mPipOverlay);
             }
-            mPipOverlay = null;
+            clearContentOverlay();
         }
         if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             // Avoid double removal, which is fatal.
@@ -1905,6 +1915,20 @@
         if (callback != null) callback.run();
     }
 
+    void clearContentOverlay() {
+        mPipOverlay = null;
+        mAppBounds.setEmpty();
+    }
+
+    void setContentOverlay(@Nullable SurfaceControl leash, @NonNull Rect appBounds) {
+        mPipOverlay = leash;
+        if (mPipOverlay != null) {
+            mAppBounds.set(appBounds);
+        } else {
+            mAppBounds.setEmpty();
+        }
+    }
+
     private void resetShadowRadius() {
         if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             // mLeash is undefined when in PipTransitionState.UNDEFINED
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index f5f15d8..89dcc4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -141,8 +141,6 @@
     /** Whether the PIP window has fade out for fixed rotation. */
     private boolean mHasFadeOut;
 
-    private Rect mInitBounds = new Rect();
-
     /** Used for setting transform to a transaction from animator. */
     private final PipAnimationController.PipTransactionHandler mTransactionConsumer =
             new PipAnimationController.PipTransactionHandler() {
@@ -465,12 +463,13 @@
                     mSurfaceTransactionHelper.crop(tx, leash, destinationBounds)
                             .resetScale(tx, leash, destinationBounds)
                             .round(tx, leash, true /* applyCornerRadius */);
-                    if (mPipOrganizer.mPipOverlay != null && !mInitBounds.isEmpty()) {
+                    final Rect appBounds = mPipOrganizer.mAppBounds;
+                    if (mPipOrganizer.mPipOverlay != null && !appBounds.isEmpty()) {
                         // Resetting the scale for pinned task while re-adjusting its crop,
                         // also scales the overlay. So we need to update the overlay leash too.
                         Rect overlayBounds = new Rect(destinationBounds);
                         final int overlaySize = PipContentOverlay.PipAppIconOverlay
-                                .getOverlaySize(mInitBounds, destinationBounds);
+                                .getOverlaySize(appBounds, destinationBounds);
 
                         overlayBounds.offsetTo(
                                 (destinationBounds.width() - overlaySize) / 2,
@@ -479,7 +478,6 @@
                                 mPipOrganizer.mPipOverlay, overlayBounds);
                     }
                 }
-                mInitBounds.setEmpty();
                 wct.setBoundsChangeTransaction(taskInfo.token, tx);
             }
             final int displayRotation = taskInfo.getConfiguration().windowConfiguration
@@ -617,7 +615,7 @@
         // if overlay is present remove it immediately, as exit transition came before it faded out
         if (mPipOrganizer.mPipOverlay != null) {
             startTransaction.remove(mPipOrganizer.mPipOverlay);
-            clearPipOverlay();
+            mPipOrganizer.clearContentOverlay();
         }
         if (pipChange == null) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -951,9 +949,6 @@
         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
         final Rect currentBounds = pipChange.getStartAbsBounds();
 
-        // Cache the start bounds for overlay manipulations as a part of finishCallback.
-        mInitBounds.set(currentBounds);
-
         int rotationDelta = deltaRotation(startRotation, endRotation);
         Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
                 taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
@@ -1022,7 +1017,7 @@
         } else {
             throw new RuntimeException("Unrecognized animation type: " + enterAnimationType);
         }
-        mPipOrganizer.mPipOverlay = animator.getContentOverlayLeash();
+        mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds);
         animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration);
@@ -1073,10 +1068,6 @@
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     "%s: SwipePipToHome should not use fixed rotation %d", TAG, mEndFixedRotation);
         }
-        Rect appBounds = pipTaskInfo.configuration.windowConfiguration.getAppBounds();
-        if (mFixedRotationState == FIXED_ROTATION_CALLBACK && appBounds != null) {
-            mInitBounds.set(appBounds);
-        }
         final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mPipOverlay;
         if (swipePipToHomeOverlay != null) {
             // Launcher fade in the overlay on top of the fullscreen Task. It is possible we
@@ -1106,7 +1097,7 @@
         sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
         if (swipePipToHomeOverlay != null) {
             mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay,
-                    this::clearPipOverlay /* callback */, false /* withStartDelay */);
+                    null /* callback */, false /* withStartDelay */);
         }
         mPipTransitionState.setInSwipePipToHomeTransition(false);
     }
@@ -1250,10 +1241,6 @@
         mPipMenuController.updateMenuBounds(destinationBounds);
     }
 
-    private void clearPipOverlay() {
-        mPipOrganizer.mPipOverlay = null;
-    }
-
     @Override
     public void dump(PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 63f20fd..238e6b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -982,8 +982,9 @@
     }
 
     private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay) {
-        mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay);
+            SurfaceControl overlay, Rect appBounds) {
+        mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay,
+                appBounds);
     }
 
     private void abortSwipePipToHome(int taskId, ComponentName componentName) {
@@ -1280,13 +1281,13 @@
 
         @Override
         public void stopSwipePipToHome(int taskId, ComponentName componentName,
-                Rect destinationBounds, SurfaceControl overlay) {
+                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
             if (overlay != null) {
                 overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
             }
             executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
                     (controller) -> controller.stopSwipePipToHome(
-                            taskId, componentName, destinationBounds, overlay));
+                            taskId, componentName, destinationBounds, overlay, appBounds));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 7b57097..880d952 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -23,12 +23,15 @@
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.getComponent;
+import static com.android.wm.shell.common.split.SplitScreenUtils.getShortcutComponent;
 import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
 import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
@@ -47,6 +50,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -171,6 +176,8 @@
     private final ShellTaskOrganizer mTaskOrganizer;
     private final SyncTransactionQueue mSyncQueue;
     private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final LauncherApps mLauncherApps;
     private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
     private final ShellExecutor mMainExecutor;
     private final SplitScreenImpl mImpl = new SplitScreenImpl();
@@ -186,7 +193,8 @@
     private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
     private final Optional<DesktopTasksController> mDesktopTasksController;
     private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
-    private final String[] mAppsSupportMultiInstances;
+    // A static allow list of apps which support multi-instance
+    private final String[] mAppsSupportingMultiInstance;
 
     @VisibleForTesting
     StageCoordinator mStageCoordinator;
@@ -220,6 +228,8 @@
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
+        mPackageManager = context.getPackageManager();
+        mLauncherApps = context.getSystemService(LauncherApps.class);
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
         mDisplayController = displayController;
@@ -242,7 +252,7 @@
 
         // TODO(255224696): Remove the config once having a way for client apps to opt-in
         //                  multi-instances split.
-        mAppsSupportMultiInstances = mContext.getResources()
+        mAppsSupportingMultiInstance = mContext.getResources()
                 .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
     }
 
@@ -266,12 +276,15 @@
             WindowDecorViewModel windowDecorViewModel,
             DesktopTasksController desktopTasksController,
             ShellExecutor mainExecutor,
-            StageCoordinator stageCoordinator) {
+            StageCoordinator stageCoordinator,
+            String[] appsSupportingMultiInstance) {
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
+        mPackageManager = context.getPackageManager();
+        mLauncherApps = context.getSystemService(LauncherApps.class);
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
         mDisplayController = displayController;
@@ -288,8 +301,7 @@
         mStageCoordinator = stageCoordinator;
         mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
         shellInit.addInitCallback(this::onInit, this);
-        mAppsSupportMultiInstances = mContext.getResources()
-                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
+        mAppsSupportingMultiInstance = appsSupportingMultiInstance;
     }
 
     public SplitScreen asSplitScreen() {
@@ -588,7 +600,8 @@
 
         if (samePackage(packageName, getPackageName(reverseSplitPosition(position)),
                 user.getIdentifier(), getUserId(reverseSplitPosition(position)))) {
-            if (supportMultiInstancesSplit(packageName)) {
+            if (supportsMultiInstanceSplit(getShortcutComponent(packageName, shortcutId, user,
+                    mLauncherApps))) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else if (isSplitScreenVisible()) {
@@ -609,7 +622,7 @@
                 activityOptions.toBundle(), user);
     }
 
-    void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+    void startShortcutAndTaskWithLegacyTransition(@NonNull ShortcutInfo shortcutInfo,
             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
             RemoteAnimationAdapter adapter, InstanceId instanceId) {
@@ -621,7 +634,7 @@
         final int userId1 = shortcutInfo.getUserId();
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
+            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -640,7 +653,7 @@
                 instanceId);
     }
 
-    void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
+    void startShortcutAndTask(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1,
             int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
             @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition,
             InstanceId instanceId) {
@@ -653,7 +666,7 @@
         final int userId1 = shortcutInfo.getUserId();
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -692,7 +705,7 @@
         final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                 fillInIntent = new Intent();
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -722,7 +735,7 @@
         final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
         boolean setSecondIntentMultipleTask = false;
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent))) {
                 setSecondIntentMultipleTask = true;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
@@ -757,7 +770,7 @@
         final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
         final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 fillInIntent2 = new Intent();
@@ -794,7 +807,7 @@
                 ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
         boolean setSecondIntentMultipleTask = false;
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 setSecondIntentMultipleTask = true;
@@ -856,7 +869,7 @@
             return;
         }
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
-            if (supportMultiInstancesSplit(packageName1)) {
+            if (supportsMultiInstanceSplit(getComponent(intent))) {
                 // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
                 // the split and there is no reusable background task.
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
@@ -915,16 +928,63 @@
         return taskInfo != null ? taskInfo.userId : -1;
     }
 
+    /**
+     * Returns whether a specific component desires to be launched in multiple instances for
+     * split screen.
+     */
     @VisibleForTesting
-    boolean supportMultiInstancesSplit(String packageName) {
-        if (packageName != null) {
-            for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
-                if (mAppsSupportMultiInstances[i].equals(packageName)) {
-                    return true;
-                }
+    boolean supportsMultiInstanceSplit(@Nullable ComponentName componentName) {
+        if (componentName == null || componentName.getPackageName() == null) {
+            // TODO(b/262864589): Handle empty component case
+            return false;
+        }
+
+        // Check the pre-defined allow list
+        final String packageName = componentName.getPackageName();
+        for (int i = 0; i < mAppsSupportingMultiInstance.length; i++) {
+            if (mAppsSupportingMultiInstance[i].equals(packageName)) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "application=%s in allowlist supports multi-instance", packageName);
+                return true;
             }
         }
 
+        // Check the activity property first
+        try {
+            final PackageManager.Property activityProp = mPackageManager.getProperty(
+                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName);
+            // If the above call doesn't throw a NameNotFoundException, then the activity property
+            // should override the application property value
+            if (activityProp.isBoolean()) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "activity=%s supports multi-instance", componentName);
+                return activityProp.getBoolean();
+            } else {
+                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Warning: property=%s for activity=%s has non-bool type=%d",
+                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName,
+                        activityProp.getType());
+            }
+        } catch (PackageManager.NameNotFoundException nnfe) {
+            // Not specified in the activity, fall through
+        }
+
+        // Check the application property otherwise
+        try {
+            final PackageManager.Property appProp = mPackageManager.getProperty(
+                    PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName);
+            if (appProp.isBoolean()) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "application=%s supports multi-instance", packageName);
+                return appProp.getBoolean();
+            } else {
+                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Warning: property=%s for application=%s has non-bool type=%d",
+                        PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.getType());
+            }
+        } catch (PackageManager.NameNotFoundException nnfe) {
+            // Not specified in either application or activity
+        }
         return false;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index f58aeac..a666e20 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -73,6 +73,7 @@
 import com.android.internal.graphics.palette.Palette;
 import com.android.internal.graphics.palette.Quantizer;
 import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
+import com.android.internal.policy.PhoneWindow;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.IconProvider;
@@ -245,16 +246,19 @@
         } else {
             windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
         }
-        params.layoutInDisplayCutoutMode = a.getInt(
-                R.styleable.Window_windowLayoutInDisplayCutoutMode,
-                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
-        params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
-        a.recycle();
 
         final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
         final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
                 ? windowInfo.targetActivityInfo
                 : taskInfo.topActivityInfo;
+        params.layoutInDisplayCutoutMode = a.getInt(
+                R.styleable.Window_windowLayoutInDisplayCutoutMode,
+                PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */)
+                        ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                        : params.layoutInDisplayCutoutMode);
+        params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
+        a.recycle();
+
         final int displayId = taskInfo.displayId;
         // Assumes it's safe to show starting windows of launched apps while
         // the keyguard is being hidden. This is okay because starting windows never show
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 9f20f49..db84513 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -21,9 +21,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
@@ -84,7 +84,7 @@
     private UnfoldTransitionHandler mUnfoldHandler;
     private ActivityEmbeddingController mActivityEmbeddingController;
 
-    private class MixedTransition {
+    private static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
         /** Both the display and split-state (enter/exit) is changing */
@@ -175,7 +175,6 @@
             joinFinishArgs(wct);
 
             if (mInFlightSubAnimations == 0) {
-                mActiveTransitions.remove(MixedTransition.this);
                 mFinishCB.onTransitionFinished(mFinishWCT);
             }
         }
@@ -401,8 +400,12 @@
                 final MixedTransition keyguardMixed =
                         new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
                 mActiveTransitions.add(keyguardMixed);
-                final boolean hasAnimateKeyguard = animateKeyguard(keyguardMixed, info,
-                        startTransaction, finishTransaction, finishCallback);
+                Transitions.TransitionFinishCallback callback = wct -> {
+                    mActiveTransitions.remove(keyguardMixed);
+                    finishCallback.onTransitionFinished(wct);
+                };
+                final boolean hasAnimateKeyguard = animateKeyguard(
+                        keyguardMixed, info, startTransaction, finishTransaction, callback);
                 if (hasAnimateKeyguard) {
                     ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                             "Converting mixed transition into a keyguard transition");
@@ -420,27 +423,34 @@
 
         if (mixed == null) return false;
 
-        if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
-            return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) {
-            return animateEnterPipFromActivityEmbedding(mixed, info, startTransaction,
-                    finishTransaction, finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+        final MixedTransition chosenTransition = mixed;
+        Transitions.TransitionFinishCallback callback = wct -> {
+            mActiveTransitions.remove(chosenTransition);
+            finishCallback.onTransitionFinished(wct);
+        };
+
+        if (chosenTransition.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+            return animateEnterPipFromSplit(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType
+                == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) {
+            return animateEnterPipFromActivityEmbedding(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
             return false;
-        } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
-            final boolean handledToPip = animateOpenIntentWithRemoteAndPip(mixed, info,
-                    startTransaction, finishTransaction, finishCallback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
+            final boolean handledToPip = animateOpenIntentWithRemoteAndPip(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
             // Consume the transition on remote handler if the leftover handler already handle this
             // transition. And if it cannot, the transition will be handled by remote handler, so
             // don't consume here.
             // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
-            if (handledToPip && mixed.mHasRequestToRemote
-                    && mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+            if (handledToPip && chosenTransition.mHasRequestToRemote
+                    && chosenTransition.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
                 mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
             }
             return handledToPip;
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
+        } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                 final TransitionInfo.Change change = info.getChanges().get(i);
                 // Pip auto-entering info might be appended to recent transition like pressing
@@ -449,28 +459,29 @@
                 if (mPipHandler.isEnteringPip(change, info.getType())
                         && mSplitHandler.getSplitItemPosition(change.getLastParent())
                         != SPLIT_POSITION_UNDEFINED) {
-                    return animateEnterPipFromSplit(mixed, info, startTransaction,
-                            finishTransaction, finishCallback);
+                    return animateEnterPipFromSplit(
+                            chosenTransition, info, startTransaction, finishTransaction, callback);
                 }
             }
 
-            return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
-            return animateKeyguard(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
-            return animateRecentsDuringKeyguard(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
-            return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction,
-                    finishCallback);
-        } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
-            return animateUnfold(mixed, info, startTransaction, finishTransaction, finishCallback);
+            return animateRecentsDuringSplit(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_KEYGUARD) {
+            return animateKeyguard(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
+            return animateRecentsDuringKeyguard(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+            return animateRecentsDuringDesktop(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
+        } else if (chosenTransition.mType == MixedTransition.TYPE_UNFOLD) {
+            return animateUnfold(
+                    chosenTransition, info, startTransaction, finishTransaction, callback);
         } else {
-            mActiveTransitions.remove(mixed);
+            mActiveTransitions.remove(chosenTransition);
             throw new IllegalStateException("Starting mixed animation without a known mixed type? "
-                    + mixed.mType);
+                    + chosenTransition.mType);
         }
     }
 
@@ -727,7 +738,11 @@
         final MixedTransition mixed = new MixedTransition(
                 MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
         mActiveTransitions.add(mixed);
-        return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback);
+        Transitions.TransitionFinishCallback callback = wct -> {
+            mActiveTransitions.remove(mixed);
+            finishCallback.onTransitionFinished(wct);
+        };
+        return animateEnterPipFromSplit(mixed, info, startT, finishT, callback);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt
new file mode 100644
index 0000000..955660c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.split
+
+import android.content.ComponentName
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.wm.shell.ShellTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class SplitScreenUtilsTests : ShellTestCase() {
+
+    @Test
+    fun getShortcutComponent_nullShortcuts() {
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(null)
+        }
+        assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    @Test
+    fun getShortcutComponent_noShortcuts() {
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(ArrayList<ShortcutInfo>())
+        }
+        assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    @Test
+    fun getShortcutComponent_validShortcut() {
+        val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
+        val shortcutInfo = ShortcutInfo.Builder(context, "id").setActivity(component).build()
+        val launcherApps = mock(LauncherApps::class.java).also {
+            `when`(it.getShortcuts(any(), any())).thenReturn(arrayListOf(shortcutInfo))
+        }
+        assertEquals(component, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE,
+                TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps))
+    }
+
+    companion object {
+        val TEST_PACKAGE = "com.android.wm.shell.common.split"
+        val TEST_ACTIVITY = "TestActivity";
+        val TEST_SHORTCUT_ID = "test_shortcut_1"
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 855b7ee..12a5594 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
+import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
@@ -36,6 +37,8 @@
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -46,8 +49,10 @@
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 
 import androidx.test.annotation.UiThreadTest;
@@ -55,6 +60,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
@@ -91,6 +97,10 @@
 @RunWith(AndroidJUnit4.class)
 public class SplitScreenControllerTests extends ShellTestCase {
 
+    private static final String TEST_PACKAGE = "com.android.wm.shell.splitscreen";
+    private static final String TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.splitscreen.fake";
+    private static final String TEST_ACTIVITY = "TestActivity";
+
     @Mock ShellInit mShellInit;
     @Mock ShellCommandHandler mShellCommandHandler;
     @Mock ShellTaskOrganizer mTaskOrganizer;
@@ -118,6 +128,8 @@
     public void setup() {
         assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
         MockitoAnnotations.initMocks(this);
+        String[] appsSupportingMultiInstance = mContext.getResources()
+                .getStringArray(R.array.config_appsSupportMultiInstancesSplit);
         mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
                 mMainExecutor));
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
@@ -125,7 +137,8 @@
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
                 mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
-                mDesktopTasksController, mMainExecutor, mStageCoordinator));
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                appsSupportingMultiInstance));
     }
 
     @Test
@@ -200,7 +213,7 @@
 
     @Test
     public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
-        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -237,12 +250,13 @@
 
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
                 isNull());
-        verify(mSplitScreenController, never()).supportMultiInstancesSplit(any());
+        verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator, never()).switchSplitPosition(any());
     }
 
     @Test
     public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
@@ -259,14 +273,14 @@
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
                 SPLIT_POSITION_TOP_OR_LEFT, null);
-        verify(mSplitScreenController, never()).supportMultiInstancesSplit(any());
+        verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
                 isNull());
     }
 
     @Test
     public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
-        doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doReturn(false).when(mSplitScreenController).supportsMultiInstanceSplit(any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -283,6 +297,130 @@
         verify(mStageCoordinator).switchSplitPosition(anyString());
     }
 
+    @Test
+    public void supportsMultiInstanceSplit_inStaticAllowList() {
+        String[] allowList = { TEST_PACKAGE };
+        SplitScreenController controller = new SplitScreenController(mContext, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                allowList);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_notInStaticAllowList() {
+        String[] allowList = { TEST_PACKAGE };
+        SplitScreenController controller = new SplitScreenController(mContext, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                allowList);
+        ComponentName component = new ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY);
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_activityPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        PackageManager.Property activityProp = new PackageManager.Property("", true, "", "");
+        doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", false, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect activity property to override application property
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_activityPropertyFalseApplicationPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        PackageManager.Property activityProp = new PackageManager.Property("", false, "", "");
+        doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", true, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect activity property to override application property
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component));
+        PackageManager.Property appProp = new PackageManager.Property("", true, "", "");
+        doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+                eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        // Expect fall through to app property
+        assertEquals(true, controller.supportsMultiInstanceSplit(component));
+    }
+
+    @Test
+    public void supportsMultiInstanceSplit_noActivityOrAppProperty()
+            throws PackageManager.NameNotFoundException {
+        Context context = spy(mContext);
+        ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        PackageManager pm = mock(PackageManager.class);
+        doReturn(pm).when(context).getPackageManager();
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component));
+        doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty(
+                eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component.getPackageName()));
+
+        SplitScreenController controller = new SplitScreenController(context, mShellInit,
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mDesktopTasksController, mMainExecutor, mStageCoordinator,
+                new String[0]);
+        assertEquals(false, controller.supportsMultiInstanceSplit(component));
+    }
+
     private Intent createStartIntent(String activityName) {
         Intent intent = new Intent();
         intent.setComponent(new ComponentName(mContext, activityName));
diff --git a/location/api/current.txt b/location/api/current.txt
index 0c23d8c..c55676b 100644
--- a/location/api/current.txt
+++ b/location/api/current.txt
@@ -414,7 +414,7 @@
     field public static final int TYPE_GPS_L5CNAV = 259; // 0x103
     field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L1 = 1795; // 0x703
     field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L5 = 1794; // 0x702
-    field @Deprecated public static final int TYPE_IRN_L5CA = 1793; // 0x701
+    field public static final int TYPE_IRN_L5CA = 1793; // 0x701
     field public static final int TYPE_QZS_L1CA = 1025; // 0x401
     field public static final int TYPE_SBS = 513; // 0x201
     field public static final int TYPE_UNKNOWN = 0; // 0x0
diff --git a/location/java/android/location/GnssNavigationMessage.java b/location/java/android/location/GnssNavigationMessage.java
index 5e3f803..7a667ae 100644
--- a/location/java/android/location/GnssNavigationMessage.java
+++ b/location/java/android/location/GnssNavigationMessage.java
@@ -78,9 +78,7 @@
     public static final int TYPE_GAL_F = 0x0602;
     /**
      * NavIC L5 C/A message contained in the structure.
-     * @deprecated deprecated.
      */
-    @Deprecated
     public static final int TYPE_IRN_L5CA = 0x0701;
     /** NavIC L5 message contained in the structure. */
     @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1)
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 0eabe66..838630f 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -943,6 +943,10 @@
                         .append(getId())
                         .append(", name=")
                         .append(getName())
+                        .append(", type=")
+                        .append(getDeviceTypeString(getType()))
+                        .append(", isSystem=")
+                        .append(isSystemRoute())
                         .append(", features=")
                         .append(getFeatures())
                         .append(", iconUri=")
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
index f44b161..0e40db2 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml
@@ -14,10 +14,11 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<resources>
+<resources
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
     <style name="TextAppearance.PreferenceTitle.SettingsLib"
            parent="@android:style/TextAppearance.Material.Subhead">
-        <item name="android:textColor">@color/settingslib_text_color_primary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
         <item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item>
         <item name="android:textSize">20sp</item>
     </style>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index c143390..b7f2c1e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -34,6 +34,15 @@
         end = itemPaddingEnd,
         bottom = itemPaddingVertical,
     )
+    val textFieldPadding = PaddingValues(
+        start = itemPaddingStart,
+        end = itemPaddingEnd,
+    )
+    val menuFieldPadding = PaddingValues(
+        start = itemPaddingStart,
+        end = itemPaddingEnd,
+        bottom = itemPaddingVertical,
+    )
     val itemPaddingAround = 8.dp
     val itemDividerHeight = 32.dp
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
index 0d6c064..f6692a3 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
@@ -51,7 +51,7 @@
         onExpandedChange = { expanded = it },
         modifier = Modifier
             .width(350.dp)
-            .padding(SettingsDimension.itemPadding),
+            .padding(SettingsDimension.menuFieldPadding),
     ) {
         OutlinedTextField(
             // The `menuAnchor` modifier must be passed to the text field for correctness.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
index 5d248e1..ba8e354 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -63,7 +63,7 @@
         onExpandedChange = { expanded = it },
         modifier = Modifier
             .width(350.dp)
-            .padding(SettingsDimension.itemPadding)
+            .padding(SettingsDimension.menuFieldPadding)
             .onSizeChanged { dropDownWidth = it.width },
     ) {
         OutlinedTextField(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
index e0dd4e1..2ce3c66 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
@@ -42,7 +42,7 @@
     OutlinedTextField(
         modifier = Modifier
             .fillMaxWidth()
-            .padding(SettingsDimension.itemPadding),
+            .padding(SettingsDimension.textFieldPadding),
         value = value,
         onValueChange = onTextChange,
         label = {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
index 0757df3..3102a00 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt
@@ -52,7 +52,7 @@
     var visibility by remember { mutableStateOf(false) }
     OutlinedTextField(
         modifier = Modifier
-            .padding(SettingsDimension.itemPadding)
+            .padding(SettingsDimension.menuFieldPadding)
             .fillMaxWidth(),
         value = value,
         onValueChange = onTextChange,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index ebcca42..5925492 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -184,10 +184,6 @@
             return false;
         }
 
-        if (mCurrentConnectedDevice != null) {
-            mCurrentConnectedDevice.disconnect();
-        }
-
         device.setState(MediaDeviceState.STATE_CONNECTING);
         mInfoMediaManager.connectToDevice(device);
         return true;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index f2d9d14..0c4cf76 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -396,12 +396,6 @@
     }
 
     /**
-     * Stop transfer MediaDevice
-     */
-    public void disconnect() {
-    }
-
-    /**
      * Set current device's state
      */
     public void setState(@LocalMediaManager.MediaDeviceState int state) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 999e8d5..9a7d4f1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -147,7 +147,6 @@
         mLocalMediaManager.registerCallback(mCallback);
         assertThat(mLocalMediaManager.connectDevice(device)).isTrue();
 
-        verify(currentDevice).disconnect();
         verify(mInfoMediaManager).connectToDevice(device);
     }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 2e39adc..add3134 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -93,6 +93,7 @@
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE,
         Settings.Global.Wearable.CLOCKWORK_24HR_TIME,
+        Settings.Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED,
         Settings.Global.Wearable.MUTE_WHEN_OFF_BODY_ENABLED,
         Settings.Global.Wearable.AMBIENT_ENABLED,
         Settings.Global.Wearable.AMBIENT_TILT_TO_WAKE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 5022395..c0a0760 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -450,6 +450,8 @@
         VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
index bd99a8b..74fd828 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java
@@ -99,7 +99,6 @@
                 "kiwi",
                 "latency_tracker",
                 "launcher",
-                "launcher_lily",
                 "leaked_animator",
                 "lmkd_native",
                 "location",
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 42107b7..d3a89f4 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -157,7 +157,7 @@
         "SystemUI-res",
         "WifiTrackerLib",
         "WindowManager-Shell",
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
         "SystemUICommon",
         "SystemUICustomizationLib",
         "SystemUILogLib",
@@ -274,7 +274,7 @@
     static_libs: [
         "SystemUI-res",
         "WifiTrackerLib",
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
         "SystemUIPluginLib",
         "SystemUISharedLib",
         "SystemUICustomizationLib",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a03fa9b..7443e4c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -84,6 +84,7 @@
     <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
     <uses-permission android:name="android.permission.LOCATION_HARDWARE" />
     <uses-permission android:name="android.permission.NETWORK_FACTORY" />
+    <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
     <!-- Physical hardware -->
     <uses-permission android:name="android.permission.MANAGE_USB" />
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 8438051..872187a 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -23,7 +23,7 @@
 
 android_library {
 
-    name: "SystemUIAnimationLib",
+    name: "PlatformAnimationLib",
     use_resource_processor: true,
 
     srcs: [
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 42d088f..9a4347d 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -30,7 +30,7 @@
     ],
 
     static_libs: [
-        "SystemUIAnimationLib",
+        "PlatformAnimationLib",
 
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index 927fd8e..1d18496 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -30,8 +30,8 @@
         "src/**/*.aidl",
     ],
     static_libs: [
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "androidx.dynamicanimation_dynamicanimation",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 75d1869..a9ee405 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -68,6 +68,8 @@
 
     override val isSingleCarrier = MutableStateFlow(true)
 
+    override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList())
+
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
 
@@ -80,8 +82,12 @@
     override val isForceHidden = MutableStateFlow(false)
 
     /** Always returns a new fake interactor */
-    override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
-        return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it }
+    override fun getMobileConnectionInteractorForSubId(subId: Int): FakeMobileIconInteractor {
+        return FakeMobileIconInteractor(tableLogBuffer).also {
+            interactorCache[subId] = it
+            // Also update the icons
+            icons.value = interactorCache.values.toList()
+        }
     }
 
     /**
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 0537f17..9063a02 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -46,8 +46,8 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx-constraintlayout_constraintlayout",
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUICommon",
         "SystemUILogLib",
         "androidx.annotation_annotation",
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index 7ab44e7..73874a0 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -44,6 +44,8 @@
                 android:ellipsize="marquee"
                 android:focusable="true"
                 android:gravity="center_vertical"
+                android:textDirection="locale"
+                android:textAlignment="viewStart"
                 android:singleLine="true"
                 android:textAppearance="@style/TextAppearance.QS.Status.Build"
                 android:visibility="gone" />
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 3a26ebf..05106c9 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -51,8 +51,8 @@
     ],
     static_libs: [
         "BiometricsSharedLib",
+        "PlatformAnimationLib",
         "PluginCoreLib",
-        "SystemUIAnimationLib",
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "SystemUISharedLib-Keyguard",
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8b992fc..b2d7052 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -91,6 +91,7 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.satellite.SatelliteManager;
 import android.view.Choreographer;
 import android.view.CrossWindowBlurListeners;
 import android.view.IWindowManager;
@@ -712,4 +713,10 @@
                 ServiceManager.getService(Context.URI_GRANTS_SERVICE)
         );
     }
+
+    @Provides
+    @Singleton
+    static Optional<SatelliteManager> provideSatelliteManager(Context context) {
+        return Optional.ofNullable(context.getSystemService(SatelliteManager.class));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index ddd7d67..51b94dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -189,6 +189,7 @@
     public void setBrightnessView(@NonNull View view) {
         if (mBrightnessView != null) {
             removeView(mBrightnessView);
+            mChildrenLayoutTop.remove(mBrightnessView);
             mMovableContentStartIndex--;
         }
         addView(view, 0);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 5eb9620..ef58a60 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -56,14 +56,18 @@
     private final QSCustomizerController mQsCustomizerController;
     private final QSTileRevealController.Factory mQsTileRevealControllerFactory;
     private final FalsingManager mFalsingManager;
-    private final BrightnessController mBrightnessController;
-    private final BrightnessSliderController mBrightnessSliderController;
-    private final BrightnessMirrorHandler mBrightnessMirrorHandler;
+    private BrightnessController mBrightnessController;
+    private BrightnessSliderController mBrightnessSliderController;
+    private BrightnessMirrorHandler mBrightnessMirrorHandler;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private boolean mListening;
 
     private final boolean mSceneContainerEnabled;
 
+    private int mLastDensity;
+    private final BrightnessSliderController.Factory mBrightnessSliderControllerFactory;
+    private final BrightnessController.Factory mBrightnessControllerFactory;
+
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
@@ -93,6 +97,8 @@
         mQsCustomizerController = qsCustomizerController;
         mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
         mFalsingManager = falsingManager;
+        mBrightnessSliderControllerFactory = brightnessSliderFactory;
+        mBrightnessControllerFactory = brightnessControllerFactory;
 
         mBrightnessSliderController = brightnessSliderFactory.create(getContext(), mView);
         mView.setBrightnessView(mBrightnessSliderController.getRootView());
@@ -100,6 +106,7 @@
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController);
         mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mLastDensity = view.getResources().getConfiguration().densityDpi;
         mSceneContainerEnabled = sceneContainerFlags.isEnabled();
     }
 
@@ -147,11 +154,31 @@
     @Override
     protected void onConfigurationChanged() {
         mView.updateResources();
+        int newDensity = mView.getResources().getConfiguration().densityDpi;
+        if (newDensity != mLastDensity) {
+            mLastDensity = newDensity;
+            reinflateBrightnessSlider();
+        }
+
         if (mView.isListening()) {
             refreshAllTiles();
         }
     }
 
+    private void reinflateBrightnessSlider() {
+        mBrightnessController.unregisterCallbacks();
+        mBrightnessSliderController =
+                mBrightnessSliderControllerFactory.create(getContext(), mView);
+        mView.setBrightnessView(mBrightnessSliderController.getRootView());
+        mBrightnessController = mBrightnessControllerFactory.create(mBrightnessSliderController);
+        mBrightnessMirrorHandler.setBrightnessController(mBrightnessController);
+        mBrightnessSliderController.init();
+        if (mListening) {
+            mBrightnessController.registerCallbacks();
+        }
+    }
+
+
     @Override
     protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) {
         ((PagedTileLayout) mView.getOrCreateTileLayout())
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
index 51aa339..701d814 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
@@ -19,9 +19,16 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController
 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener
 
-class BrightnessMirrorHandler(private val brightnessController: MirroredBrightnessController) {
+class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController) {
 
-    private var mirrorController: BrightnessMirrorController? = null
+    var mirrorController: BrightnessMirrorController? = null
+        private set
+
+    var brightnessController: MirroredBrightnessController = brightnessController
+        set(value) {
+            field = value
+            updateBrightnessMirror()
+        }
 
     private val brightnessMirrorListener = BrightnessMirrorListener { updateBrightnessMirror() }
 
@@ -33,7 +40,7 @@
         mirrorController?.removeCallback(brightnessMirrorListener)
     }
 
-    fun setController(controller: BrightnessMirrorController) {
+    fun setController(controller: BrightnessMirrorController?) {
         mirrorController?.removeCallback(brightnessMirrorListener)
         mirrorController = controller
         mirrorController?.addCallback(brightnessMirrorListener)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 9ae4195..d7cbe5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
@@ -40,11 +41,13 @@
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusIconDisplayable;
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
@@ -432,6 +435,10 @@
 
                 case TYPE_MOBILE_NEW:
                     return addNewMobileIcon(index, slot, holder.getTag());
+
+                case TYPE_BINDABLE:
+                    // Safe cast, since only BindableIconHolders can set this tag on themselves
+                    return addBindableIcon((BindableIconHolder) holder, index);
             }
 
             return null;
@@ -446,6 +453,18 @@
             return view;
         }
 
+        /**
+         * ModernStatusBarViews can be created and bound, and thus do not need to update their
+         *  drawable by sending multiple calls to setIcon. Instead, by using a bindable
+         * icon view, we can simply create the icon when requested and allow the
+         * ViewBinder to control its visual state.
+         */
+        protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) {
+            ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
+            mGroup.addView(view, index, onCreateLayoutParams());
+            return view;
+        }
+
         protected StatusIconDisplayable addNewWifiIcon(int index, String slot) {
             ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
             mGroup.addView(view, index, onCreateLayoutParams());
@@ -530,6 +549,7 @@
                     return;
                 case TYPE_MOBILE_NEW:
                 case TYPE_WIFI_NEW:
+                case TYPE_BINDABLE:
                     // Nothing, the new icons update themselves
                     return;
                 default:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 0f4d68c..4f148f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -38,8 +38,11 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusIconDisplayable;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.tuner.TunerService;
@@ -83,7 +86,8 @@
             TunerService tunerService,
             DumpManager dumpManager,
             StatusBarIconList statusBarIconList,
-            StatusBarPipelineFlags statusBarPipelineFlags
+            StatusBarPipelineFlags statusBarPipelineFlags,
+            BindableIconsRegistry modernIconsRegistry
     ) {
         mStatusBarIconList = statusBarIconList;
         mContext = context;
@@ -94,6 +98,28 @@
         tunerService.addTunable(this, ICON_HIDE_LIST);
         demoModeController.addCallback(this);
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
+        addModernBindableIcons(modernIconsRegistry);
+    }
+
+    /**
+     * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound
+     * upon being added to any icon group. Because their view policy does not require subsequent
+     * calls to setIcon(), we can simply register them all statically here and not have to build
+     * CoreStartables for each modern icon.
+     *
+     * @param registry a statically defined provider of the modern icons
+     */
+    private void addModernBindableIcons(BindableIconsRegistry registry) {
+        List<BindableIcon> icons = registry.getBindableIcons();
+
+        // Initialization point for the bindable (modern) icons. These icons get their own slot
+        // allocated immediately, and are required to control their own display properties
+        for (BindableIcon i : icons) {
+            if (i.getShouldBindIcon()) {
+                addBindableIcon(i);
+            }
+        }
     }
 
     /** */
@@ -182,6 +208,17 @@
         mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder));
     }
 
+    void addBindableIcon(BindableIcon icon) {
+        StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
+        // Expected to be null
+        if (existingHolder == null) {
+            BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer());
+            setIcon(icon.getSlot(), bindableIcon);
+        } else {
+            Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
+        }
+    }
+
     /** */
     @Override
     public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
index 5b55a1e..bef0b28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -21,23 +21,24 @@
 import android.os.UserHandle
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
 
 /** Wraps [com.android.internal.statusbar.StatusBarIcon] so we can still have a uniform list */
-class StatusBarIconHolder private constructor() {
-    @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW)
+open class StatusBarIconHolder private constructor() {
+    @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW, TYPE_BINDABLE)
     @Retention(AnnotationRetention.SOURCE)
     internal annotation class IconType
 
     var icon: StatusBarIcon? = null
 
     @IconType
-    var type = TYPE_ICON
-        private set
+    open var type = TYPE_ICON
+        internal set
 
     var tag = 0
         private set
 
-    var isVisible: Boolean
+    open var isVisible: Boolean
         get() =
             when (type) {
                 TYPE_ICON -> icon!!.visible
@@ -45,6 +46,7 @@
                 // The new pipeline controls visibilities via the view model and
                 // view binder, so
                 // this is effectively an unused return value.
+                TYPE_BINDABLE,
                 TYPE_MOBILE_NEW,
                 TYPE_WIFI_NEW -> true
                 else -> true
@@ -55,6 +57,7 @@
             }
             when (type) {
                 TYPE_ICON -> icon!!.visible = visible
+                TYPE_BINDABLE,
                 TYPE_MOBILE_NEW,
                 TYPE_WIFI_NEW -> {}
             }
@@ -94,6 +97,9 @@
         )
         const val TYPE_WIFI_NEW = 4
 
+        /** Only applicable to [BindableIconHolder] */
+        const val TYPE_BINDABLE = 5
+
         /** Returns a human-readable string representing the given type. */
         fun getTypeString(@IconType type: Int): String {
             return when (type) {
@@ -154,4 +160,25 @@
             return holder
         }
     }
+
+    /**
+     * Subclass of StatusBarIconHolder that is responsible only for the registration of an icon into
+     * the [StatusBarIconList]. A bindable icon takes care of its own display, including hiding
+     * itself under the correct conditions.
+     *
+     * StatusBarIconController will register all available bindable icons on init (see
+     * [BindableIconsRepository]), and will ignore any call to setIcon for these.
+     *
+     * [initializer] a view creator that can bind the relevant view models to the created view.
+     */
+    class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() {
+        override var type: Int = TYPE_BINDABLE
+
+        /** This is unused, as bindable icons use their own view binders to control visibility */
+        override var isVisible: Boolean = true
+
+        override fun toString(): String {
+            return ("StatusBarIconHolder(type=BINDABLE)")
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index e1fd37f..89a2fb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
@@ -42,6 +44,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
@@ -76,8 +80,16 @@
     abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel
 
     @Binds
+    abstract fun bindableIconsRepository(impl: BindableIconsRegistryImpl): BindableIconsRegistry
+
+    @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
+    @Binds
+    abstract fun deviceBasedSatelliteRepository(
+        impl: DeviceBasedSatelliteRepositoryImpl
+    ): DeviceBasedSatelliteRepository
+
     @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
 
     @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
new file mode 100644
index 0000000..e3c3139
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.icons.shared
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import javax.inject.Inject
+
+/**
+ * Bindable status bar icons represent icon descriptions which can be registered with
+ * StatusBarIconController and can also create their own bindings. A bound icon is responsible for
+ * its own updates via the [repeatWhenAttached] view lifecycle utility. Thus,
+ * StatusBarIconController can (and will) ignore any call to setIcon.
+ *
+ * In other words, these icons are bound once (at controller init) and they will control their
+ * visibility on their own (while their overall appearance remains at the discretion of
+ * StatusBarIconController, via the ModernStatusBarViewBinding interface).
+ */
+interface BindableIconsRegistry {
+    val bindableIcons: List<BindableIcon>
+}
+
+@SysUISingleton
+class BindableIconsRegistryImpl
+@Inject
+constructor(
+/** Bindables go here */
+) : BindableIconsRegistry {
+    /**
+     * Adding the injected bindables to this list will get them registered with
+     * StatusBarIconController
+     */
+    override val bindableIcons: List<BindableIcon> = listOf()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt
new file mode 100644
index 0000000..9d0d838
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.icons.shared.model
+
+/**
+ * A BindableIcon describes a status bar icon that can be housed in the [ModernStatusBarView]
+ * created by [initializer]. They can be registered statically for [BindableIconsRepositoryImpl].
+ *
+ * Typical usage would be to create an (@SysUISingleton) adapter class that implements the
+ * interface. For example:
+ * ```
+ * @SysuUISingleton
+ * class MyBindableIconAdapter
+ * @Inject constructor(
+ *     // deps
+ *     val viewModel: MyViewModel
+ * ) : BindableIcon {
+ *     override val slot = "icon_slot_name"
+ *
+ *     override val initializer = ModernStatusBarViewCreator() {
+ *         SingleBindableStatusBarIconView.createView(context).also { iconView ->
+ *             MyIconViewBinder.bind(iconView, viewModel)
+ *         }
+ *     }
+ *
+ *     override fun shouldBind() = Flags.myFlag()
+ * }
+ * ```
+ *
+ * By defining this adapter (and injecting it into the repository), we get our icon registered with
+ * the legacy StatusBarIconController while proxying all updates to the view binder that is created
+ * elsewhere.
+ *
+ * Note that the initializer block defines a closure that can pull in the viewModel dependency
+ * without us having to store it directly in the icon controller.
+ */
+interface BindableIcon {
+    val slot: String
+    val initializer: ModernStatusBarViewCreator
+    val shouldBindIcon: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
new file mode 100644
index 0000000..dbd5c1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.icons.shared.model
+
+import android.content.Context
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
+
+/**
+ * Defined as an interface (as opposed to a typealias) to simplify calling from java.
+ * [ModernStatusBarViewCreator.createAndBind] should return a constructed and bound
+ * [ModernStatusBarView].
+ */
+fun interface ModernStatusBarViewCreator {
+    fun createAndBind(context: Context): ModernStatusBarView
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index dad4093..39135c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -71,6 +71,12 @@
     /** List of subscriptions, potentially filtered for CBRS */
     val filteredSubscriptions: Flow<List<SubscriptionModel>>
 
+    /**
+     * The current list of [MobileIconInteractor]s associated with the current list of
+     * [filteredSubscriptions]
+     */
+    val icons: StateFlow<List<MobileIconInteractor>>
+
     /** True if the active mobile data subscription has data enabled */
     val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
 
@@ -259,6 +265,13 @@
         }
     }
 
+    override val icons =
+        filteredSubscriptions
+            .mapLatest { subs ->
+                subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
     /**
      * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
      * validated bit from the old active network (A) while data is changing to the new one (B).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..ad8b810
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data
+
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Device-based satellite refers to the capability of a device to connect directly to a satellite
+ * network. This is in contrast to carrier-based satellite connectivity, which is a property of a
+ * given mobile data subscription.
+ */
+interface DeviceBasedSatelliteRepository {
+    /** See [SatelliteConnectionState] for available states */
+    val connectionState: Flow<SatelliteConnectionState>
+
+    /** 0-4 level (similar to wifi and mobile) */
+    // @IntRange(from = 0, to = 4)
+    val signalStrength: Flow<Int>
+
+    /** Clients must observe this property, as device-based satellite is location-dependent */
+    val isSatelliteAllowedForCurrentLocation: Flow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
new file mode 100644
index 0000000..8fc8b2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import android.os.OutcomeReceiver
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteStateCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.time.SystemClock
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/**
+ * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to
+ * get one
+ */
+private typealias SupportedSatelliteManager = SatelliteManager
+
+/**
+ * "Supported" here means supported by the device. The value of this should be stable during the
+ * process lifetime.
+ */
+private sealed interface SatelliteSupport {
+    /** Not yet fetched */
+    data object Unknown : SatelliteSupport
+
+    /**
+     * SatelliteManager says that this mode is supported. Note that satellite manager can never be
+     * null now
+     */
+    data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport
+
+    /**
+     * Either we were told that there is no support for this feature, or the manager is null, or
+     * some other exception occurred while querying for support.
+     */
+    data object NotSupported : SatelliteSupport
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    companion object {
+        /** Convenience function to switch to the supported flow */
+        fun <T> Flow<SatelliteSupport>.whenSupported(
+            supported: (SatelliteManager) -> Flow<T>,
+            orElse: Flow<T>,
+        ): Flow<T> = flatMapLatest {
+            when (it) {
+                is Supported -> supported(it.satelliteManager)
+                else -> orElse
+            }
+        }
+    }
+}
+
+/**
+ * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ *
+ * First, there is an availability bit that we are tracking via [SatelliteManager]. See
+ * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
+ * this bit is that there is no callback that exists. Therefore we implement a simple polling
+ * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
+ * [POLLING_INTERVAL_MS]) and see what the current state is.
+ *
+ * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * [SatelliteSupport] for details on how we track the state. What's worth noting here is that
+ * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
+ * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
+ * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a
+ * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
+ * us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
+ *
+ * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
+ * is still booting up or anything like that. We can tune or remove this behavior in the future if
+ * necessary.
+ */
+@SysUISingleton
+class DeviceBasedSatelliteRepositoryImpl
+@Inject
+constructor(
+    satelliteManagerOpt: Optional<SatelliteManager>,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val systemClock: SystemClock,
+) : DeviceBasedSatelliteRepository {
+
+    private val satelliteManager: SatelliteManager?
+
+    override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
+
+    // Some calls into satellite manager will throw exceptions if it is not supported.
+    // This is never expected to change after boot, but may need to be retried in some cases
+    private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
+
+    init {
+        satelliteManager = satelliteManagerOpt.getOrNull()
+
+        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+        if (satelliteManager != null) {
+            // First, check that satellite is supported on this device
+            scope.launch {
+                ensureMinUptime(systemClock, MIN_UPTIME)
+                satelliteSupport.value = satelliteManager.checkSatelliteSupported()
+
+                // We only need to check location availability if this mode is supported
+                if (satelliteSupport.value is Supported) {
+                    isSatelliteAllowedForCurrentLocation.subscriptionCount
+                        .map { it > 0 }
+                        .distinctUntilChanged()
+                        .collectLatest { hasSubscribers ->
+                            if (hasSubscribers) {
+                                /*
+                                 * As there is no listener available for checking satellite allowed,
+                                 * we must poll. Defaulting to polling at most once every hour while
+                                 * active. Subsequent OOS events will restart the job, so a flaky
+                                 * connection might cause more frequent checks.
+                                 */
+                                while (true) {
+                                    checkIsSatelliteAllowed()
+                                    delay(POLLING_INTERVAL_MS)
+                                }
+                            }
+                        }
+                }
+            }
+        } else {
+            satelliteSupport.value = NotSupported
+        }
+    }
+
+    override val connectionState =
+        satelliteSupport.whenSupported(
+            supported = ::connectionStateFlow,
+            orElse = flowOf(SatelliteConnectionState.Off)
+        )
+
+    // By using the SupportedSatelliteManager here, we expect registration never to fail
+    private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
+        conflatedCallbackFlow {
+                val cb = SatelliteStateCallback { state ->
+                    trySend(SatelliteConnectionState.fromModemState(state))
+                }
+
+                sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+
+                awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+            }
+            .flowOn(bgDispatcher)
+
+    override val signalStrength =
+        satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+
+    // By using the SupportedSatelliteManager here, we expect registration never to fail
+    private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
+        conflatedCallbackFlow {
+                val cb = NtnSignalStrengthCallback { signalStrength ->
+                    trySend(signalStrength.level)
+                }
+
+                sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+
+                awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+            }
+            .flowOn(bgDispatcher)
+
+    /** Fire off a request to check for satellite availability. Always runs on the bg context */
+    private suspend fun checkIsSatelliteAllowed() =
+        withContext(bgDispatcher) {
+            satelliteManager?.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                bgDispatcher.asExecutor(),
+                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+                    override fun onError(e: SatelliteManager.SatelliteException) {
+                        android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+                        isSatelliteAllowedForCurrentLocation.value = false
+                    }
+
+                    override fun onResult(allowed: Boolean) {
+                        isSatelliteAllowedForCurrentLocation.value = allowed
+                    }
+                }
+            )
+        }
+
+    private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
+        suspendCancellableCoroutine { continuation ->
+            val cb =
+                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+                    override fun onResult(supported: Boolean) {
+                        continuation.resume(
+                            if (supported) {
+                                Supported(satelliteManager = this@checkSatelliteSupported)
+                            } else {
+                                NotSupported
+                            }
+                        )
+                    }
+
+                    override fun onError(error: SatelliteManager.SatelliteException) {
+                        // Assume that an error means it's not supported
+                        continuation.resume(NotSupported)
+                    }
+                }
+
+            requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb)
+        }
+
+    companion object {
+        // TTL for satellite polling is one hour
+        const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
+
+        // Let the system boot up and stabilize before we check for system support
+        const val MIN_UPTIME: Long = 1000 * 60
+
+        private const val TAG = "DeviceBasedSatelliteRepo"
+
+        /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */
+        private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) {
+            val timeTilMinUptime =
+                uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
+            if (timeTilMinUptime > 0) {
+                delay(timeTilMinUptime)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
new file mode 100644
index 0000000..8779577
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
+
+import com.android.internal.telephony.flags.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class DeviceBasedSatelliteInteractor
+@Inject
+constructor(
+    val repo: DeviceBasedSatelliteRepository,
+    iconsInteractor: MobileIconsInteractor,
+    @Application scope: CoroutineScope,
+) {
+    /** Must be observed by any UI showing Satellite iconography */
+    val isSatelliteAllowed =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.isSatelliteAllowedForCurrentLocation
+            } else {
+                flowOf(false)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    /** See [SatelliteConnectionState] for relevant states */
+    val connectionState =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.connectionState
+            } else {
+
+                flowOf(SatelliteConnectionState.Off)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
+
+    /** 0-4 description of the connection strength */
+    val signalStrength =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                repo.signalStrength
+            } else {
+                flowOf(0)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+
+    /** When all connections are considered OOS, satellite connectivity is potentially valid */
+    val areAllConnectionsOutOfService =
+        if (Flags.oemEnabledSatelliteFlag()) {
+                iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) {
+                    isInServiceList ->
+                    isInServiceList.all { !it }
+                }
+            } else {
+                flowOf(false)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+}
+
+/**
+ * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the
+ * top-level flow. Re-emits if the list changes, or any of the intermediate values change.
+ *
+ * Provides a way to connect the reactivity of the top-level flow with the reactivity of an
+ * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver(
+    crossinline selector: (R) -> Flow<S>,
+    crossinline transform: (Array<S>) -> T
+): Flow<T> {
+    return map { list -> list.map { selector(it) } }
+        .flatMapLatest { newFlows -> combine(newFlows) { newVals -> transform(newVals) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
new file mode 100644
index 0000000..bfe2941
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.shared.model
+
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+
+enum class SatelliteConnectionState {
+    // State is unknown or undefined
+    Unknown,
+    // Radio is off
+    Off,
+    // Radio is on, but not yet connected
+    On,
+    // Radio is connected, aka satellite is available for use
+    Connected;
+
+    companion object {
+        // TODO(b/316635648): validate these states. We don't need the level of granularity that
+        //  SatelliteManager gives us.
+        fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) =
+            when (modemState) {
+                // Transferring data is connected
+                SATELLITE_MODEM_STATE_CONNECTED,
+                SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING,
+                SATELLITE_MODEM_STATE_DATAGRAM_RETRYING -> Connected
+
+                // Modem is on but not connected
+                SATELLITE_MODEM_STATE_IDLE,
+                SATELLITE_MODEM_STATE_LISTENING,
+                SATELLITE_MODEM_STATE_NOT_CONNECTED -> On
+
+                // Consider unavailable equivalent to Off
+                SATELLITE_MODEM_STATE_UNAVAILABLE,
+                SATELLITE_MODEM_STATE_OFF -> Off
+
+                // Else, we don't know what's up
+                SATELLITE_MODEM_STATE_UNKNOWN -> Unknown
+                else -> Unknown
+            }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
index 6f04f36..f6a8243 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
@@ -23,6 +23,9 @@
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
@@ -49,14 +52,15 @@
         iconList = StatusBarIconList(arrayOf())
         underTest =
             StatusBarIconControllerImpl(
-                context,
-                commandQueue,
-                mock(),
-                mock(),
-                mock(),
-                mock(),
-                iconList,
-                mock(),
+                /* context = */ context,
+                /* commandQueue = */ commandQueue,
+                /* demoModeController = */ mock(),
+                /* configurationController = */ mock(),
+                /* tunerService = */ mock(),
+                /* dumpManager = */ mock(),
+                /* statusBarIconList = */ iconList,
+                /* statusBarPipelineFlags = */ mock(),
+                /* modernIconsRegistry = */ mock(),
             )
         underTest.addIconGroup(iconGroup)
         val commandQueueCallbacksCaptor = kotlinArgumentCaptor<CommandQueue.Callbacks>()
@@ -366,6 +370,31 @@
         assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX")
     }
 
+    @Test
+    fun bindableIcons_addedOnInit() {
+        val fakeIcon = FakeBindableIcon("test_slot")
+
+        iconList = StatusBarIconList(arrayOf())
+
+        // WHEN there are registered icons
+        underTest =
+            StatusBarIconControllerImpl(
+                /* context = */ context,
+                /* commandQueue = */ commandQueue,
+                /* demoModeController = */ mock(),
+                /* configurationController = */ mock(),
+                /* tunerService = */ mock(),
+                /* dumpManager = */ mock(),
+                /* statusBarIconList = */ iconList,
+                /* statusBarPipelineFlags = */ mock(),
+                /* modernIconsRegistry = */ FakeBindableIconsRegistry(listOf(fakeIcon)),
+            )
+
+        // THEN they are properly added to the list on init
+        assertThat(iconList.getIconHolder("test_slot", 0))
+            .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java)
+    }
+
     private fun createExternalIcon(): StatusBarIcon {
         return StatusBarIcon(
             "external.package",
@@ -377,3 +406,20 @@
         )
     }
 }
+
+class FakeBindableIconsRegistry(
+    override val bindableIcons: List<BindableIcon>,
+) : BindableIconsRegistry
+
+class FakeBindableIcon(
+    override val slot: String,
+    override val shouldBindIcon: Boolean = true,
+) : BindableIcon {
+    // Track initialized so we can know that our icon was properly bound
+    var hasInitialized = false
+
+    override val initializer = ModernStatusBarViewCreator { _ ->
+        hasInitialized = true
+        mock()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 0ff6f20..ca31623 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
@@ -108,7 +109,8 @@
                 mock(TunerService.class),
                 mock(DumpManager.class),
                 mock(StatusBarIconList.class),
-                flags
+                flags,
+                mock(BindableIconsRegistry.class)
         );
 
         iconController.addIconGroup(manager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
new file mode 100644
index 0000000..a906a89
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import android.os.OutcomeReceiver
+import android.os.Process
+import android.telephony.satellite.NtnSignalStrength
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+import android.telephony.satellite.SatelliteManager.SatelliteException
+import android.telephony.satellite.SatelliteStateCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl
+
+    @Mock private lateinit var satelliteManager: SatelliteManager
+
+    private val systemClock = FakeSystemClock()
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun nullSatelliteManager_usesDefaultValues() =
+        testScope.runTest {
+            setupDefaultRepo()
+            underTest =
+                DeviceBasedSatelliteRepositoryImpl(
+                    Optional.empty(),
+                    dispatcher,
+                    testScope.backgroundScope,
+                    systemClock,
+                )
+
+            val connectionState by collectLastValue(underTest.connectionState)
+            val strength by collectLastValue(underTest.signalStrength)
+            val allowed by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(connectionState).isEqualTo(SatelliteConnectionState.Off)
+            assertThat(strength).isEqualTo(0)
+            assertThat(allowed).isFalse()
+        }
+
+    @Test
+    fun connectionState_mapsFromSatelliteModemState() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.connectionState)
+            runCurrent()
+            val callback =
+                withArgCaptor<SatelliteStateCallback> {
+                    verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture())
+                }
+
+            // Mapping from modem state to SatelliteConnectionState is rote, just run all of the
+            // possibilities here
+
+            // Off states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNAVAILABLE)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_IDLE)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_LISTENING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_NOT_CONNECTED)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+            // Connected states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_CONNECTED)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_RETRYING)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+            // Unknown states
+            callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNKNOWN)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+            // Garbage value (for completeness' sake)
+            callback.onSatelliteModemStateChanged(123456)
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+        }
+
+    @Test
+    fun signalStrength_readsSatelliteManagerState() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.signalStrength)
+            runCurrent()
+            val callback =
+                withArgCaptor<NtnSignalStrengthCallback> {
+                    verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
+                }
+
+            assertThat(latest).isNull()
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
+            assertThat(latest).isEqualTo(1)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(2))
+            assertThat(latest).isEqualTo(2)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(3))
+            assertThat(latest).isEqualTo(3)
+
+            callback.onNtnSignalStrengthChanged(NtnSignalStrength(4))
+            assertThat(latest).isEqualTo(4)
+        }
+
+    @Test
+    fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is allowed in this location
+            val allowed = true
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not allowed in this location
+            val allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isSatelliteAllowed_pollsOnTimeout() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not allowed in this location
+            var allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+
+            // WHEN satellite becomes enabled
+            allowed = true
+
+            // WHEN the timeout has not yet been reached
+            advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+            // THEN the value is still false
+            assertThat(latest).isFalse()
+
+            // WHEN time advances beyond the polling interval
+            advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
+
+            // THEN then new value is emitted
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
+
+            var latest: Boolean? = false
+            var job =
+                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+            // GIVEN satellite is not allowed in this location
+            var allowed = false
+
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onResult(allowed)
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            assertThat(latest).isFalse()
+
+            // WHEN satellite becomes enabled
+            allowed = true
+
+            // WHEN the job is restarted
+            advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+            job.cancel()
+            job =
+                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+            // THEN the value is re-fetched
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isSatelliteAllowed_falseWhenErrorOccurs() =
+        testScope.runTest {
+            setupDefaultRepo()
+            doAnswer {
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onError(SatelliteException(1 /* unused */))
+                    null
+                }
+                .`when`(satelliteManager)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    any(),
+                    any<OutcomeReceiver<Boolean, SatelliteException>>()
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun satelliteNotSupported_listenersAreNotRegistered() =
+        testScope.runTest {
+            setupDefaultRepo()
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the manager is not asked for the information, and default values are returned
+            verify(satelliteManager, never()).registerForSatelliteModemStateChanged(any(), any())
+            verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any())
+        }
+
+    @Test
+    fun repoDoesNotCheckForSupportUntilMinUptime() =
+        testScope.runTest {
+            // GIVEN we init 100ms after sysui starts up
+            setUpRepo(
+                uptime = 100,
+                satMan = satelliteManager,
+                satelliteSupported = true,
+            )
+
+            // WHEN data is requested
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN we have not yet talked to satellite manager, since we are well before MIN_UPTIME
+            Mockito.verifyZeroInteractions(satelliteManager)
+
+            // WHEN enough time has passed
+            systemClock.advanceTime(MIN_UPTIME)
+            runCurrent()
+
+            // THEN we finally register with the satellite manager
+            verify(satelliteManager).registerForSatelliteModemStateChanged(any(), any())
+        }
+
+    private fun setUpRepo(
+        uptime: Long = MIN_UPTIME,
+        satMan: SatelliteManager? = satelliteManager,
+        satelliteSupported: Boolean = true,
+    ) {
+        doAnswer {
+                val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                callback.onResult(satelliteSupported)
+            }
+            .whenever(satelliteManager)
+            .requestIsSatelliteSupported(any(), any())
+
+        systemClock.setUptimeMillis(Process.getStartUptimeMillis() + uptime)
+
+        underTest =
+            DeviceBasedSatelliteRepositoryImpl(
+                if (satMan != null) Optional.of(satMan) else Optional.empty(),
+                dispatcher,
+                testScope.backgroundScope,
+                systemClock,
+            )
+    }
+
+    // Set system time to MIN_UPTIME and create a repo with satellite supported
+    private fun setupDefaultRepo() {
+        setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..5fa2d33
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.prod
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState.Off
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDeviceBasedSatelliteRepository() : DeviceBasedSatelliteRepository {
+    override val connectionState = MutableStateFlow(Off)
+
+    override val signalStrength = MutableStateFlow(0)
+
+    override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
new file mode 100644
index 0000000..e010b86
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+
+@SmallTest
+class DeviceBasedSatelliteInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteInteractor
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private val iconsInteractor =
+        FakeMobileIconsInteractor(
+            FakeMobileMappingsProxy(),
+            mock(),
+        )
+
+    private val repo = FakeDeviceBasedSatelliteRepository()
+
+    @Before
+    fun setUp() {
+        mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+        underTest =
+            DeviceBasedSatelliteInteractor(
+                repo,
+                iconsInteractor,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun isSatelliteAllowed_falseWhenNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isSatelliteAllowed_trueWhenAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isSatelliteAllowed_offWhenFlagIsOff() =
+        testScope.runTest {
+            // GIVEN feature is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+            // WHEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // THEN the interactor returns false due to the flag value
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun connectionState_matchesRepositoryValue() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.connectionState)
+
+            // Off
+            repo.connectionState.value = SatelliteConnectionState.Off
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On
+            repo.connectionState.value = SatelliteConnectionState.On
+            assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+            // Connected
+            repo.connectionState.value = SatelliteConnectionState.Connected
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+            // Unknown
+            repo.connectionState.value = SatelliteConnectionState.Unknown
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+        }
+
+    @Test
+    fun connectionState_offWhenFeatureIsDisabled() =
+        testScope.runTest {
+            // GIVEN the flag is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.connectionState)
+
+            // THEN the state is always Off, regardless of status in system_server
+
+            // Off
+            repo.connectionState.value = SatelliteConnectionState.Off
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // On
+            repo.connectionState.value = SatelliteConnectionState.On
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // Connected
+            repo.connectionState.value = SatelliteConnectionState.Connected
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+            // Unknown
+            repo.connectionState.value = SatelliteConnectionState.Unknown
+            assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+        }
+
+    @Test
+    fun signalStrength_matchesRepo() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalStrength)
+
+            repo.signalStrength.value = 1
+            assertThat(latest).isEqualTo(1)
+
+            repo.signalStrength.value = 2
+            assertThat(latest).isEqualTo(2)
+
+            repo.signalStrength.value = 3
+            assertThat(latest).isEqualTo(3)
+
+            repo.signalStrength.value = 4
+            assertThat(latest).isEqualTo(4)
+        }
+
+    @Test
+    fun signalStrength_zeroWhenDisabled() =
+        testScope.runTest {
+            // GIVEN the flag is enabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.signalStrength)
+
+            // THEN the value is always 0, regardless of what the system says
+            repo.signalStrength.value = 1
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 2
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 3
+            assertThat(latest).isEqualTo(0)
+
+            repo.signalStrength.value = 4
+            assertThat(latest).isEqualTo(0)
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsOos_yes() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connections
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN all of the connections are OOS
+            i1.isInService.value = false
+            i2.isInService.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_oneConnectionOos_yes() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 1 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all of the connections are OOS
+            i1.isInService.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_oneConnectionInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 1 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all of the connections are NOT OOS
+            i1.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN at least 1 connection is NOT OOS.
+            i1.isInService.value = false
+            i2.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_twoConnectionsInService_no() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            // WHEN all connections are NOT OOS.
+            i1.isInService.value = true
+            i2.isInService.value = true
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun areAllConnectionsOutOfService_falseWhenFlagIsOff() =
+        testScope.runTest {
+            // GIVEN the flag is disabled
+            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+            // Remake the interactor so the flag is read
+            underTest =
+                DeviceBasedSatelliteInteractor(
+                    repo,
+                    iconsInteractor,
+                    testScope.backgroundScope,
+                )
+
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN a condition that should return true (all conections OOS)
+
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+            i1.isInService.value = true
+            i2.isInService.value = true
+
+            // THEN the value is still false, because the flag is off
+            assertThat(latest).isFalse()
+        }
+}
diff --git a/packages/overlays/NoCutoutOverlay/res/values/config.xml b/packages/overlays/NoCutoutOverlay/res/values/config.xml
index ed0340b..b44a153a 100644
--- a/packages/overlays/NoCutoutOverlay/res/values/config.xml
+++ b/packages/overlays/NoCutoutOverlay/res/values/config.xml
@@ -20,10 +20,17 @@
          black in software (to avoid aliasing or emulate a cutout that is not physically existent).
      -->
     <bool name="config_fillMainBuiltInDisplayCutout">false</bool>
+    <!-- Whether the display cutout region of the secondary built-in display should be forced to
+         black in software (to avoid aliasing or emulate a cutout that is not physically existent).
+     -->
+    <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool>
 
     <!-- If true, and there is a cutout on the main built in display, the cutout will be masked
          by shrinking the display such that it does not overlap the cutout area. -->
     <bool name="config_maskMainBuiltInDisplayCutout">true</bool>
+    <!-- If true, and there is a cutout on the secondary built in display, the cutout will be masked
+         by shrinking the display such that it does not overlap the cutout area. -->
+    <bool name="config_maskSecondaryBuiltInDisplayCutout">true</bool>
 
     <!-- Height of the status bar -->
     <dimen name="status_bar_height_portrait">28dp</dimen>
diff --git a/proto/src/criticalevents/critical_event_log.proto b/proto/src/criticalevents/critical_event_log.proto
index 9cda267..cffcd09 100644
--- a/proto/src/criticalevents/critical_event_log.proto
+++ b/proto/src/criticalevents/critical_event_log.proto
@@ -60,8 +60,11 @@
     JavaCrash java_crash = 5;
     NativeCrash native_crash = 6;
     SystemServerStarted system_server_started = 7;
+    InstallPackages install_packages = 8;
   }
 
+  message InstallPackages {}
+
   message SystemServerStarted {}
 
   message Watchdog {
diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
index 18cf46f..e4cc1f8 100644
--- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
+++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
@@ -136,7 +136,7 @@
         byte[] requestsPayload = new byte[buffer.getInt()];
         buffer.get(requestsPayload);
         List<SystemDataTransferRequest> restoredRequestsForUser =
-                mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload);
+                mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload, userId);
 
         // Get a list of installed packages ahead of time.
         List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
index 8fe0454..51c5fd6 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
@@ -69,7 +69,6 @@
  *   <request
  *     association_id="1"
  *     data_type="1"
- *     user_id="12"
  *     is_user_consented="true"
  *   </request>
  * </requests>
@@ -86,7 +85,6 @@
 
     private static final String XML_ATTR_ASSOCIATION_ID = "association_id";
     private static final String XML_ATTR_DATA_TYPE = "data_type";
-    private static final String XML_ATTR_USER_ID = "user_id";
     private static final String XML_ATTR_IS_USER_CONSENTED = "is_user_consented";
 
     private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds
@@ -169,12 +167,12 @@
      * Parse the byte array containing XML information of system data transfer requests into
      * an array list of requests.
      */
-    public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload) {
+    public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload, int userId) {
         try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) {
             final TypedXmlPullParser parser = Xml.resolvePullParser(in);
             XmlUtils.beginDocument(parser, XML_TAG_REQUESTS);
 
-            return readRequestsFromXml(parser);
+            return readRequestsFromXml(parser, userId);
         } catch (XmlPullParserException | IOException e) {
             Slog.e(LOG_TAG, "Error while reading requests file", e);
             return new ArrayList<>();
@@ -226,7 +224,7 @@
                 final TypedXmlPullParser parser = Xml.resolvePullParser(in);
                 XmlUtils.beginDocument(parser, XML_TAG_REQUESTS);
 
-                return readRequestsFromXml(parser);
+                return readRequestsFromXml(parser, userId);
             } catch (XmlPullParserException | IOException e) {
                 Slog.e(LOG_TAG, "Error while reading requests file", e);
                 return new ArrayList<>();
@@ -236,7 +234,8 @@
 
     @NonNull
     private ArrayList<SystemDataTransferRequest> readRequestsFromXml(
-            @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException {
+            @NonNull TypedXmlPullParser parser, int userId)
+            throws XmlPullParserException, IOException {
         if (!isStartOfTag(parser, XML_TAG_REQUESTS)) {
             throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_REQUESTS);
         }
@@ -249,14 +248,15 @@
                 break;
             }
             if (isStartOfTag(parser, XML_TAG_REQUEST)) {
-                requests.add(readRequestFromXml(parser));
+                requests.add(readRequestFromXml(parser, userId));
             }
         }
 
         return requests;
     }
 
-    private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser)
+    private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser,
+            int userId)
             throws XmlPullParserException, IOException {
         if (!isStartOfTag(parser, XML_TAG_REQUEST)) {
             throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_REQUEST);
@@ -264,7 +264,6 @@
 
         final int associationId = readIntAttribute(parser, XML_ATTR_ASSOCIATION_ID);
         final int dataType = readIntAttribute(parser, XML_ATTR_DATA_TYPE);
-        final int userId = readIntAttribute(parser, XML_ATTR_USER_ID);
         final boolean isUserConsented = readBooleanAttribute(parser, XML_ATTR_IS_USER_CONSENTED);
 
         switch (dataType) {
@@ -321,7 +320,6 @@
 
         writeIntAttribute(serializer, XML_ATTR_ASSOCIATION_ID, request.getAssociationId());
         writeIntAttribute(serializer, XML_ATTR_DATA_TYPE, request.getDataType());
-        writeIntAttribute(serializer, XML_ATTR_USER_ID, request.getUserId());
         writeBooleanAttribute(serializer, XML_ATTR_IS_USER_CONSENTED, request.isUserConsented());
 
         serializer.endTag(null, XML_TAG_REQUEST);
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index d089b05..2f9b6a5 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -55,9 +55,7 @@
     @GuardedBy("mCameras")
     private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
 
-    public VirtualCameraController() {
-        connectVirtualCameraService();
-    }
+    public VirtualCameraController() {}
 
     @VisibleForTesting
     VirtualCameraController(IVirtualCameraService virtualCameraService) {
diff --git a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
index 0814375..816c349 100644
--- a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
+++ b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
@@ -29,6 +29,7 @@
 import com.android.server.criticalevents.nano.CriticalEventProto;
 import com.android.server.criticalevents.nano.CriticalEventProto.AppNotResponding;
 import com.android.server.criticalevents.nano.CriticalEventProto.HalfWatchdog;
+import com.android.server.criticalevents.nano.CriticalEventProto.InstallPackages;
 import com.android.server.criticalevents.nano.CriticalEventProto.JavaCrash;
 import com.android.server.criticalevents.nano.CriticalEventProto.NativeCrash;
 import com.android.server.criticalevents.nano.CriticalEventProto.SystemServerStarted;
@@ -142,6 +143,13 @@
         return System.currentTimeMillis();
     }
 
+    /** Logs when one or more packages are installed. */
+    public void logInstallPackagesStarted() {
+        CriticalEventProto event = new CriticalEventProto();
+        event.setInstallPackages(new InstallPackages());
+        log(event);
+    }
+
     /** Logs when system server started. */
     public void logSystemServerStarted() {
         CriticalEventProto event = new CriticalEventProto();
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index 9fcaa1e..d50a43a 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -33,6 +33,7 @@
     private final float mSdrBrightness;
 
     private final float mMaxBrightness;
+    private final float mMinBrightness;
     private final BrightnessReason mBrightnessReason;
     private final String mDisplayBrightnessStrategyName;
     private final boolean mShouldUseAutoBrightness;
@@ -50,6 +51,7 @@
         mShouldUseAutoBrightness = builder.getShouldUseAutoBrightness();
         mIsSlowChange = builder.isSlowChange();
         mMaxBrightness = builder.getMaxBrightness();
+        mMinBrightness = builder.getMinBrightness();
         mCustomAnimationRate = builder.getCustomAnimationRate();
         mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting();
     }
@@ -105,6 +107,13 @@
     }
 
     /**
+     * @return minimum allowed brightness
+     */
+    public float getMinBrightness() {
+        return mMinBrightness;
+    }
+
+    /**
      * @return custom animation rate
      */
     public float getCustomAnimationRate() {
@@ -131,6 +140,7 @@
         stringBuilder.append(getShouldUseAutoBrightness());
         stringBuilder.append("\n    isSlowChange:").append(mIsSlowChange);
         stringBuilder.append("\n    maxBrightness:").append(mMaxBrightness);
+        stringBuilder.append("\n    minBrightness:").append(mMinBrightness);
         stringBuilder.append("\n    customAnimationRate:").append(mCustomAnimationRate);
         stringBuilder.append("\n    shouldUpdateScreenBrightnessSetting:")
                 .append(mShouldUpdateScreenBrightnessSetting);
@@ -160,6 +170,7 @@
                 && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness()
                 && mIsSlowChange == otherState.isSlowChange()
                 && mMaxBrightness == otherState.getMaxBrightness()
+                && mMinBrightness == otherState.getMinBrightness()
                 && mCustomAnimationRate == otherState.getCustomAnimationRate()
                 && mShouldUpdateScreenBrightnessSetting
                     == otherState.shouldUpdateScreenBrightnessSetting();
@@ -168,7 +179,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason,
-                mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mCustomAnimationRate,
+                mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness,
+                mCustomAnimationRate,
                 mShouldUpdateScreenBrightnessSetting);
     }
 
@@ -190,6 +202,7 @@
         private boolean mShouldUseAutoBrightness;
         private boolean mIsSlowChange;
         private float mMaxBrightness;
+        private float mMinBrightness;
         private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET;
         private boolean mShouldUpdateScreenBrightnessSetting;
 
@@ -208,6 +221,7 @@
             builder.setShouldUseAutoBrightness(state.getShouldUseAutoBrightness());
             builder.setIsSlowChange(state.isSlowChange());
             builder.setMaxBrightness(state.getMaxBrightness());
+            builder.setMinBrightness(state.getMinBrightness());
             builder.setCustomAnimationRate(state.getCustomAnimationRate());
             builder.setShouldUpdateScreenBrightnessSetting(
                     state.shouldUpdateScreenBrightnessSetting());
@@ -334,6 +348,20 @@
             return mMaxBrightness;
         }
 
+        /**
+         * See {@link DisplayBrightnessState#getMinBrightness()}.
+         */
+        public Builder setMinBrightness(float minBrightness) {
+            this.mMinBrightness = minBrightness;
+            return this;
+        }
+
+        /**
+         * See {@link DisplayBrightnessState#getMinBrightness()}.
+         */
+        public float getMinBrightness() {
+            return mMinBrightness;
+        }
 
         /**
          * See {@link DisplayBrightnessState#getCustomAnimationRate()}.
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index bc3f9dd..fbac924 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1615,6 +1615,10 @@
                 if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) {
                     Slog.w(TAG, "Display created with home support but lacks "
                             + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request.");
+                } else if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
+                    Slog.w(TAG, "Display created with home support but has "
+                            + "VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, ignoring the home support "
+                            + "request.");
                 } else {
                     mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId,
                             Display.TYPE_VIRTUAL, true);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 7df6114..2d860c0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -573,10 +573,10 @@
         mBrightnessClamperController = mInjector.getBrightnessClamperController(
                 mHandler, modeChangeCallback::run,
                 new BrightnessClamperController.DisplayDeviceData(
-                mUniqueDisplayId,
-                mThermalBrightnessThrottlingDataId,
-                logicalDisplay.getPowerThrottlingDataIdLocked(),
-                mDisplayDeviceConfig), mContext, flags);
+                        mUniqueDisplayId,
+                        mThermalBrightnessThrottlingDataId,
+                        logicalDisplay.getPowerThrottlingDataIdLocked(),
+                        mDisplayDeviceConfig), mContext, flags);
         // Seed the cached brightness
         saveBrightnessInfo(getScreenBrightnessSetting());
         mAutomaticBrightnessStrategy =
@@ -1508,7 +1508,6 @@
         // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
         // we broadcast this change through setting.
         final float unthrottledBrightnessState = brightnessState;
-
         DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
                 brightnessState, slowChange);
 
@@ -1522,11 +1521,12 @@
         if (updateScreenBrightnessSetting) {
             // Tell the rest of the system about the new brightness in case we had to change it
             // for things like auto-brightness or high-brightness-mode. Note that we do this
-            // only considering maxBrightness (ignroing brightness modifiers like low power or dim)
+            // only considering maxBrightness (ignoring brightness modifiers like low power or dim)
             // so that the slider accurately represents the full possible range,
             // even if they range changes what it means in absolute terms.
             mDisplayBrightnessController.updateScreenBrightnessSetting(
-                    Math.min(unthrottledBrightnessState, clampedState.getMaxBrightness()));
+                    MathUtils.constrain(unthrottledBrightnessState,
+                            clampedState.getMinBrightness(), clampedState.getMaxBrightness()));
         }
 
         // The current brightness to use has been calculated at this point, and HbmController should
@@ -1935,8 +1935,9 @@
             @Nullable DisplayBrightnessState state) {
         synchronized (mCachedBrightnessInfo) {
             float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
-            final float minBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax);
+            float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX;
+            final float minBrightness = Math.max(stateMin, Math.min(
+                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax));
             final float maxBrightness = Math.min(
                     mBrightnessRangeController.getCurrentBrightnessMax(), stateMax);
             boolean changed = false;
@@ -1962,7 +1963,6 @@
             changed |=
                     mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
                             mBrightnessClamperController.getBrightnessMaxReason());
-
             return changed;
         }
     }
@@ -2880,6 +2880,7 @@
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
                     (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
                     mBrightnessClamperController.getBrightnessMaxReason(),
+                    // TODO: (flc) add brightnessMinReason here too.
                     (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
                     event.isRbcEnabled(),
                     (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0,
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index bcf27b4..90bad12 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -333,6 +333,8 @@
     public void stop() {
         mStopped = true;
         mPhotonicModulator.interrupt();
+        mColorFadePrepared = false;
+        mColorFadeReady = true;
         if (mColorFade != null) {
             mAsyncDestroyExecutor.execute(mColorFade::destroy);
         }
@@ -419,7 +421,8 @@
         }
     };
 
-    private final Runnable mColorFadeDrawRunnable = new Runnable() {
+    @VisibleForTesting
+    final Runnable mColorFadeDrawRunnable = new Runnable() {
         @Override
         public void run() {
             mColorFadeDrawPending = false;
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
index 8fe5f21..bc443a8 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
@@ -46,8 +46,10 @@
     public static final int MODIFIER_LOW_POWER = 0x2;
     public static final int MODIFIER_HDR = 0x4;
     public static final int MODIFIER_THROTTLED = 0x8;
+    public static final int MODIFIER_MIN_LUX = 0x10;
+    public static final int MODIFIER_MIN_USER_SET_LOWER_BOUND = 0x20;
     public static final int MODIFIER_MASK = MODIFIER_DIMMED | MODIFIER_LOW_POWER | MODIFIER_HDR
-            | MODIFIER_THROTTLED;
+            | MODIFIER_THROTTLED | MODIFIER_MIN_LUX | MODIFIER_MIN_USER_SET_LOWER_BOUND;
 
     // ADJUSTMENT_*
     // These things can happen at any point, even if the main brightness reason doesn't
@@ -131,6 +133,12 @@
         if ((mModifier & MODIFIER_THROTTLED) != 0) {
             sb.append(" throttled");
         }
+        if ((mModifier & MODIFIER_MIN_LUX) != 0) {
+            sb.append(" lux_lower_bound");
+        }
+        if ((mModifier & MODIFIER_MIN_USER_SET_LOWER_BOUND) != 0) {
+            sb.append(" user_min_pref");
+        }
         int strlen = sb.length();
         if (sb.charAt(strlen - 1) == '[') {
             sb.setLength(strlen - 2);
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
index 42ebc40..fab769e 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
@@ -30,6 +30,7 @@
 abstract class BrightnessClamper<T> {
 
     protected float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
+
     protected boolean mIsActive = false;
 
     @NonNull
@@ -75,6 +76,5 @@
         THERMAL,
         POWER,
         BEDTIME_MODE,
-        LUX,
     }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 01694dd..2c02fc6 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -58,13 +58,14 @@
     private final Executor mExecutor;
     private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers;
 
-    private final List<BrightnessModifier> mModifiers;
+    private final List<BrightnessStateModifier> mModifiers;
     private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
 
     private float mCustomAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
     @Nullable
     private Type mClamperType = null;
+
     private boolean mClamperApplied = false;
 
     public BrightnessClamperController(Handler handler,
@@ -92,7 +93,7 @@
 
         mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags,
                 context);
-        mModifiers = injector.getModifiers(context);
+        mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener);
         mOnPropertiesChangedListener =
                 properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
         start();
@@ -165,9 +166,10 @@
      * Used to dump ClampersController state.
      */
     public void dump(PrintWriter writer) {
-        writer.println("BrightnessClampersController:");
+        writer.println("BrightnessClamperController:");
         writer.println("  mBrightnessCap: " + mBrightnessCap);
         writer.println("  mClamperType: " + mClamperType);
+        writer.println("  mClamperApplied: " + mClamperApplied);
         IndentingPrintWriter ipw = new IndentingPrintWriter(writer, "    ");
         mClampers.forEach(clamper -> clamper.dump(ipw));
         mModifiers.forEach(modifier -> modifier.dump(ipw));
@@ -181,6 +183,7 @@
         mDeviceConfigParameterProvider.removeOnPropertiesChangedListener(
                 mOnPropertiesChangedListener);
         mClampers.forEach(BrightnessClamper::stop);
+        mModifiers.forEach(BrightnessStateModifier::stop);
     }
 
 
@@ -201,14 +204,14 @@
             customAnimationRate = minClamper.getCustomAnimationRate();
         }
 
-        if (mBrightnessCap != brightnessCap || mClamperType != clamperType
+        if (mBrightnessCap != brightnessCap
+                || mClamperType != clamperType
                 || mCustomAnimationRate != customAnimationRate) {
             mBrightnessCap = brightnessCap;
             mClamperType = clamperType;
             mCustomAnimationRate = customAnimationRate;
             mClamperChangeListenerExternal.onChanged();
         }
-
     }
 
     private void start() {
@@ -248,16 +251,17 @@
                 clampers.add(new BrightnessWearBedtimeModeClamper(handler, context,
                         clamperChangeListener, data));
             }
-            if (flags.isEvenDimmerEnabled()) {
-                clampers.add(new BrightnessMinClamper(handler, clamperChangeListener, context));
-            }
             return clampers;
         }
 
-        List<BrightnessModifier> getModifiers(Context context) {
-            List<BrightnessModifier> modifiers = new ArrayList<>();
+        List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
+                Handler handler, ClamperChangeListener listener) {
+            List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
+            if (flags.isEvenDimmerEnabled()) {
+                modifiers.add(new BrightnessLowLuxModifier(handler, listener, context));
+            }
             return modifiers;
         }
     }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
new file mode 100644
index 0000000..7f1f7a9
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.clamper;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.hardware.display.DisplayManagerInternal;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.utils.DebugUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Class used to prevent the screen brightness dipping below a certain value, based on current
+ * lux conditions and user preferred minimum.
+ */
+public class BrightnessLowLuxModifier implements
+        BrightnessStateModifier {
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
+    private static final String TAG = "BrightnessLowLuxModifier";
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private final SettingsObserver mSettingsObserver;
+    private final ContentResolver mContentResolver;
+    private final Handler mHandler;
+    private final BrightnessClamperController.ClamperChangeListener mChangeListener;
+    protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
+    private int mReason;
+    private float mBrightnessLowerBound;
+    private boolean mIsActive;
+
+    @VisibleForTesting
+    BrightnessLowLuxModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener listener, Context context) {
+        super();
+
+        mChangeListener = listener;
+        mHandler = handler;
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new SettingsObserver(mHandler);
+        mHandler.post(() -> {
+            start();
+        });
+    }
+
+    /**
+     * Calculates new lower bound for brightness range, based on whether the setting is active,
+     * the user defined min brightness setting, and current lux environment.
+     */
+    @VisibleForTesting
+    public void recalculateLowerBound() {
+        int userId = UserHandle.USER_CURRENT;
+        float settingNitsLowerBound = Settings.Secure.getFloatForUser(
+                mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
+                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+
+        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED,
+                /* def= */ 0, userId) == 1;
+
+        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
+        float luxBasedNitsLowerBound = 0.0f;
+
+        // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+                // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+
+        final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
+                ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
+                : BrightnessReason.MODIFIER_MIN_LUX;
+
+        // TODO: brightnessLowerBound = nitsToBrightnessSpline(nitsLowerBound);
+        final float brightnessLowerBound = PowerManager.BRIGHTNESS_MIN;
+
+        if (mBrightnessLowerBound != brightnessLowerBound
+                || mReason != reason
+                || mIsActive != isActive) {
+            mIsActive = isActive;
+            mReason = reason;
+            if (DEBUG) {
+                Slog.i(TAG, "isActive: " + isActive
+                        + ", settingNitsLowerBound: " + settingNitsLowerBound
+                        + ", lowerBound: " + brightnessLowerBound);
+            }
+            mBrightnessLowerBound = brightnessLowerBound;
+            mChangeListener.onChanged();
+        }
+    }
+
+    @VisibleForTesting
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    @VisibleForTesting
+    public int getBrightnessReason() {
+        return mReason;
+    }
+
+    @VisibleForTesting
+    public float getBrightnessLowerBound() {
+        return mBrightnessLowerBound;
+    }
+
+    void start() {
+        recalculateLowerBound();
+    }
+
+    @Override
+    public void apply(DisplayManagerInternal.DisplayPowerRequest request,
+            DisplayBrightnessState.Builder stateBuilder) {
+        stateBuilder.setMinBrightness(mBrightnessLowerBound);
+        float boundedBrightness = Math.max(mBrightnessLowerBound, stateBuilder.getBrightness());
+        stateBuilder.setBrightness(boundedBrightness);
+
+        if (BrightnessSynchronizer.floatEquals(stateBuilder.getBrightness(),
+                mBrightnessLowerBound)) {
+            stateBuilder.getBrightnessReason().addModifier(mReason);
+        }
+    }
+
+    @Override
+    public void stop() {
+        mContentResolver.unregisterContentObserver(mSettingsObserver);
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        pw.println("BrightnessLowLuxModifier:");
+        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
+        pw.println("  mIsActive=" + mIsActive);
+        pw.println("  mReason=" + mReason);
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
+                    false, this);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
+                    false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            recalculateLowerBound();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
deleted file mode 100644
index 71efca1..0000000
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness.clamper;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.display.utils.DebugUtils;
-
-import java.io.PrintWriter;
-
-/**
- * Class used to prevent the screen brightness dipping below a certain value, based on current
- * lux conditions.
- */
-public class BrightnessMinClamper extends BrightnessClamper {
-
-    // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.BrightnessMinClamper DEBUG && adb reboot'
-    private static final String TAG = "BrightnessMinClamper";
-    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
-
-    private final SettingsObserver mSettingsObserver;
-
-    ContentResolver mContentResolver;
-    private float mNitsLowerBound;
-
-    @VisibleForTesting
-    BrightnessMinClamper(Handler handler,
-            BrightnessClamperController.ClamperChangeListener listener, Context context) {
-        super(handler, listener);
-
-        mContentResolver = context.getContentResolver();
-        mSettingsObserver = new SettingsObserver(mHandler);
-        mHandler.post(() -> {
-            start();
-        });
-    }
-
-    private void recalculateLowerBound() {
-        final int userId = UserHandle.USER_CURRENT;
-        float settingNitsLowerBound = Settings.Secure.getFloatForUser(
-                mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
-                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
-
-        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
-                Settings.Secure.EVEN_DIMMER_ACTIVATED,
-                /* def= */ 0, userId) == 1;
-
-        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
-        float luxBasedNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
-        final float nitsLowerBound = Math.max(settingNitsLowerBound, luxBasedNitsLowerBound);
-
-        if (mNitsLowerBound != nitsLowerBound || mIsActive != isActive) {
-            mIsActive = isActive;
-            mNitsLowerBound = nitsLowerBound;
-            if (DEBUG) {
-                Slog.i(TAG, "mIsActive: " + mIsActive);
-            }
-            // TODO: mBrightnessCap = nitsToBrightnessSpline(mNitsLowerBound);
-            mChangeListener.onChanged();
-        }
-    }
-
-    void start() {
-        recalculateLowerBound();
-    }
-
-
-    @Override
-    Type getType() {
-        return Type.LUX;
-    }
-
-    @Override
-    void onDeviceConfigChanged() {
-        // TODO
-    }
-
-    @Override
-    void onDisplayChanged(Object displayData) {
-
-    }
-
-    @Override
-    void stop() {
-        mContentResolver.unregisterContentObserver(mSettingsObserver);
-    }
-
-    @Override
-    void dump(PrintWriter pw) {
-        pw.println("BrightnessMinClamper:");
-        pw.println("  mBrightnessCap=" + mBrightnessCap);
-        pw.println("  mIsActive=" + mIsActive);
-        pw.println("  mNitsLowerBound=" + mNitsLowerBound);
-        super.dump(pw);
-    }
-
-    private final class SettingsObserver extends ContentObserver {
-        SettingsObserver(Handler handler) {
-            super(handler);
-            mContentResolver.registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
-                    false, this);
-            mContentResolver.registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
-                    false, this);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            recalculateLowerBound();
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index 112e63d..be8fa5a 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -26,7 +26,7 @@
 /**
  * Modifies current brightness based on request
  */
-abstract class BrightnessModifier {
+abstract class BrightnessModifier implements BrightnessStateModifier {
 
     private boolean mApplied = false;
 
@@ -37,7 +37,8 @@
 
     abstract int getModifier();
 
-    void apply(DisplayManagerInternal.DisplayPowerRequest request,
+    @Override
+    public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
         // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor
         // as long as it is above the minimum threshold.
@@ -57,8 +58,14 @@
         }
     }
 
-    void dump(PrintWriter pw) {
+    @Override
+    public void dump(PrintWriter pw) {
         pw.println("BrightnessModifier:");
         pw.println("  mApplied=" + mApplied);
     }
+
+    @Override
+    public void stop() {
+        // do nothing
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
new file mode 100644
index 0000000..441ba8f
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.clamper;
+
+import android.hardware.display.DisplayManagerInternal;
+
+import com.android.server.display.DisplayBrightnessState;
+
+import java.io.PrintWriter;
+
+public interface BrightnessStateModifier {
+    /**
+     * Applies the changes to brightness state, by modifying properties of the brightness
+     * state builder.
+     * @param request
+     * @param stateBuilder
+     */
+    void apply(DisplayManagerInternal.DisplayPowerRequest request,
+            DisplayBrightnessState.Builder stateBuilder);
+
+    /**
+     * Prints contents of this brightness state modifier
+     * @param printWriter
+     */
+    void dump(PrintWriter printWriter);
+
+    /**
+     * Called when stopped. Listeners can be unregistered here.
+     */
+    void stop();
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 3244aff..db64a75 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -893,48 +893,206 @@
     }
 
     void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
-            @ConfigChangeOrigin int origin, boolean isNew) {
-        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
-        //  - USER can override anything and updates bitmask of user-modified fields;
-        //  - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
-        //  - APP can only update if not user-modified.
-        if (rule.enabled != automaticZenRule.isEnabled()) {
-            rule.snoozing = false;
-        }
-        rule.name = automaticZenRule.getName();
-        rule.condition = null;
-        rule.conditionId = automaticZenRule.getConditionId();
-        rule.enabled = automaticZenRule.isEnabled();
-        rule.modified = automaticZenRule.isModified();
-        rule.zenPolicy = automaticZenRule.getZenPolicy();
+                         @ConfigChangeOrigin int origin, boolean isNew) {
         if (Flags.modesApi()) {
-            rule.zenDeviceEffects = fixZenDeviceEffects(
-                    rule.zenDeviceEffects,
-                    automaticZenRule.getDeviceEffects(),
-                    origin);
-        }
-        rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
-                automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-        rule.configurationActivity = automaticZenRule.getConfigurationActivity();
+            // These values can always be edited by the app, so we apply changes immediately.
+            if (isNew) {
+                rule.id = ZenModeConfig.newRuleId();
+                rule.creationTime = System.currentTimeMillis();
+                rule.component = automaticZenRule.getOwner();
+                rule.pkg = pkg;
+            }
 
-        if (isNew) {
-            rule.id = ZenModeConfig.newRuleId();
-            rule.creationTime = System.currentTimeMillis();
-            rule.component = automaticZenRule.getOwner();
-            rule.pkg = pkg;
-        }
-
-        if (Flags.modesApi()) {
+            rule.condition = null;
+            rule.conditionId = automaticZenRule.getConditionId();
+            if (rule.enabled != automaticZenRule.isEnabled()) {
+                rule.snoozing = false;
+            }
+            rule.enabled = automaticZenRule.isEnabled();
+            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
             rule.allowManualInvocation = automaticZenRule.isManualInvocationAllowed();
-            rule.iconResName = drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId());
+            rule.iconResName =
+                    drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId());
             rule.triggerDescription = automaticZenRule.getTriggerDescription();
             rule.type = automaticZenRule.getType();
+            // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
+            rule.modified = automaticZenRule.isModified();
+
+            // Name is treated differently than other values:
+            // App is allowed to update name if the name was not modified by the user (even if
+            // other values have been modified). In this way, if the locale of an app changes,
+            // i18n of the rule name can still occur even if the user has customized the rule
+            // contents.
+            String previousName = rule.name;
+            if (isNew || doesOriginAlwaysUpdateValues(origin)
+                    || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
+                rule.name = automaticZenRule.getName();
+            }
+
+            // For the remaining values, rules can always have all values updated if:
+            // * the rule is newly added, or
+            // * the request comes from an origin that can always update values, like the user, or
+            // * the rule has not yet been user modified, and thus can be updated by the app.
+            boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin)
+                    || rule.canBeUpdatedByApp();
+
+            // For all other values, if updates are not allowed, we discard the update.
+            if (!updateValues) {
+                return;
+            }
+
+            // Updates the bitmasks if the origin of the change is the user.
+            boolean updateBitmask = (origin == UPDATE_ORIGIN_USER);
+
+            if (updateBitmask && !TextUtils.equals(previousName, automaticZenRule.getName())) {
+                rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+            }
+            int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
+                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            if (updateBitmask && rule.zenMode != newZenMode) {
+                rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+            }
+
+            // Updates the values in the ZenRule itself.
+            rule.zenMode = newZenMode;
+
+            // Updates the bitmask and values for all policy fields, based on the origin.
+            rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(),
+                    updateBitmask);
+            // Updates the bitmask and values for all device effect fields, based on the origin.
+            rule.zenDeviceEffects = updateZenDeviceEffects(
+                    rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(),
+                    origin == UPDATE_ORIGIN_APP, updateBitmask);
+        } else {
+            if (rule.enabled != automaticZenRule.isEnabled()) {
+                rule.snoozing = false;
+            }
+            rule.name = automaticZenRule.getName();
+            rule.condition = null;
+            rule.conditionId = automaticZenRule.getConditionId();
+            rule.enabled = automaticZenRule.isEnabled();
+            rule.modified = automaticZenRule.isModified();
+            rule.zenPolicy = automaticZenRule.getZenPolicy();
+            if (Flags.modesApi()) {
+                rule.zenDeviceEffects = updateZenDeviceEffects(
+                        rule.zenDeviceEffects,
+                        automaticZenRule.getDeviceEffects(),
+                        origin == UPDATE_ORIGIN_APP,
+                        origin == UPDATE_ORIGIN_USER);
+            }
+            rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
+                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
+
+            if (isNew) {
+                rule.id = ZenModeConfig.newRuleId();
+                rule.creationTime = System.currentTimeMillis();
+                rule.component = automaticZenRule.getOwner();
+                rule.pkg = pkg;
+            }
         }
     }
 
     /**
-     * Fix {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
-     *
+     * Returns true when fields can always be updated, based on the provided origin of an AZR
+     * change. (Note that regardless of origin, fields can always be updated if they're not already
+     * user modified.)
+     */
+    private static boolean doesOriginAlwaysUpdateValues(@ConfigChangeOrigin int origin) {
+        return origin == UPDATE_ORIGIN_USER || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+    }
+
+    /**
+     * Modifies {@link ZenPolicy} that is being stored as part of a new or updated ZenRule.
+     * Returns a policy based on {@code oldPolicy}, but with fields updated to match
+     * {@code newPolicy} where they differ, and updating the internal user-modified bitmask to
+     * track these changes, if applicable based on {@code origin}.
+     */
+    @Nullable
+    private ZenPolicy updatePolicy(@Nullable ZenPolicy oldPolicy, @Nullable ZenPolicy newPolicy,
+                                   boolean updateBitmask) {
+        // If the update is to make the policy null, we don't need to update the bitmask,
+        // because it won't be stored anywhere anyway.
+        if (newPolicy == null) {
+            return null;
+        }
+
+        // If oldPolicy is null, we compare against the default policy when determining which
+        // fields in the bitmask should be marked as updated.
+        if (oldPolicy == null) {
+            oldPolicy = mDefaultConfig.toZenPolicy();
+        }
+
+        int userModifiedFields = oldPolicy.getUserModifiedFields();
+        if (updateBitmask) {
+            if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_MESSAGES;
+            }
+            if (oldPolicy.getPriorityCallSenders() != newPolicy.getPriorityCallSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_CALLS;
+            }
+            if (oldPolicy.getPriorityConversationSenders()
+                    != newPolicy.getPriorityConversationSenders()) {
+                userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS;
+            }
+            if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) {
+                userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS;
+            }
+            if (oldPolicy.getPriorityCategoryReminders()
+                    != newPolicy.getPriorityCategoryReminders()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS;
+            }
+            if (oldPolicy.getPriorityCategoryEvents() != newPolicy.getPriorityCategoryEvents()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS;
+            }
+            if (oldPolicy.getPriorityCategoryRepeatCallers()
+                    != newPolicy.getPriorityCategoryRepeatCallers()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS;
+            }
+            if (oldPolicy.getPriorityCategoryAlarms() != newPolicy.getPriorityCategoryAlarms()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_ALARMS;
+            }
+            if (oldPolicy.getPriorityCategoryMedia() != newPolicy.getPriorityCategoryMedia()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_MEDIA;
+            }
+            if (oldPolicy.getPriorityCategorySystem() != newPolicy.getPriorityCategorySystem()) {
+                userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM;
+            }
+            // Visual effects
+            if (oldPolicy.getVisualEffectFullScreenIntent()
+                    != newPolicy.getVisualEffectFullScreenIntent()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT;
+            }
+            if (oldPolicy.getVisualEffectLights() != newPolicy.getVisualEffectLights()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS;
+            }
+            if (oldPolicy.getVisualEffectPeek() != newPolicy.getVisualEffectPeek()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_PEEK;
+            }
+            if (oldPolicy.getVisualEffectStatusBar() != newPolicy.getVisualEffectStatusBar()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_STATUS_BAR;
+            }
+            if (oldPolicy.getVisualEffectBadge() != newPolicy.getVisualEffectBadge()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_BADGE;
+            }
+            if (oldPolicy.getVisualEffectAmbient() != newPolicy.getVisualEffectAmbient()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT;
+            }
+            if (oldPolicy.getVisualEffectNotificationList()
+                    != newPolicy.getVisualEffectNotificationList()) {
+                userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST;
+            }
+        }
+
+        // After all bitmask changes have been made, sets the bitmask.
+        return new ZenPolicy.Builder(newPolicy).setUserModifiedFields(userModifiedFields).build();
+    }
+
+    /**
+     * Modifies {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
+     * Returns a {@link ZenDeviceEffects} based on {@code oldEffects}, but with fields updated to
+     * match {@code newEffects} where they differ, and updating the internal user-modified bitmask
+     * to track these changes, if applicable based on {@code origin}.
      * <ul>
      *     <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are
      *     intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them
@@ -942,38 +1100,85 @@
      * </ul>
      */
     @Nullable
-    private static ZenDeviceEffects fixZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
-            @Nullable ZenDeviceEffects newEffects, @ConfigChangeOrigin int origin) {
-        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
-        //  - USER can override anything and updates bitmask of user-modified fields;
-        //  - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
-        //  - APP can only update if not user-modified.
-        if (origin != UPDATE_ORIGIN_APP) {
-            return newEffects;
-        }
-
+    private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
+                                                           @Nullable ZenDeviceEffects newEffects,
+                                                           boolean isFromApp,
+                                                           boolean updateBitmask) {
         if (newEffects == null) {
             return null;
         }
-        if (oldEffects != null) {
-            return new ZenDeviceEffects.Builder(newEffects)
-                    .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
-                    .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
-                    .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
-                    .setShouldDisableTouch(oldEffects.shouldDisableTouch())
-                    .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
-                    .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
-                    .build();
-        } else {
-            return new ZenDeviceEffects.Builder(newEffects)
-                    .setShouldDisableAutoBrightness(false)
-                    .setShouldDisableTapToWake(false)
-                    .setShouldDisableTiltToWake(false)
-                    .setShouldDisableTouch(false)
-                    .setShouldMinimizeRadioUsage(false)
-                    .setShouldMaximizeDoze(false)
-                    .build();
+
+        // Since newEffects is not null, we want to adopt all the new provided device effects.
+        ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects);
+
+        if (isFromApp) {
+            if (oldEffects != null) {
+                // We can do this because we know we don't need to update the bitmask FROM_APP.
+                return builder
+                        .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
+                        .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
+                        .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
+                        .setShouldDisableTouch(oldEffects.shouldDisableTouch())
+                        .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
+                        .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+                        .build();
+            } else {
+                return builder
+                        .setShouldDisableAutoBrightness(false)
+                        .setShouldDisableTapToWake(false)
+                        .setShouldDisableTiltToWake(false)
+                        .setShouldDisableTouch(false)
+                        .setShouldMinimizeRadioUsage(false)
+                        .setShouldMaximizeDoze(false)
+                        .build();
+            }
         }
+
+        // If oldEffects is null, we compare against the default device effects object when
+        // determining which fields in the bitmask should be marked as updated.
+        if (oldEffects == null) {
+            oldEffects = new ZenDeviceEffects.Builder().build();
+        }
+
+        int userModifiedFields = oldEffects.getUserModifiedFields();
+        if (updateBitmask) {
+            if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE;
+            }
+            if (oldEffects.shouldSuppressAmbientDisplay()
+                    != newEffects.shouldSuppressAmbientDisplay()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_SUPPRESS_AMBIENT_DISPLAY;
+            }
+            if (oldEffects.shouldDimWallpaper() != newEffects.shouldDimWallpaper()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DIM_WALLPAPER;
+            }
+            if (oldEffects.shouldUseNightMode() != newEffects.shouldUseNightMode()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_NIGHT_MODE;
+            }
+            if (oldEffects.shouldDisableAutoBrightness()
+                    != newEffects.shouldDisableAutoBrightness()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_AUTO_BRIGHTNESS;
+            }
+            if (oldEffects.shouldDisableTapToWake() != newEffects.shouldDisableTapToWake()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TAP_TO_WAKE;
+            }
+            if (oldEffects.shouldDisableTiltToWake() != newEffects.shouldDisableTiltToWake()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TILT_TO_WAKE;
+            }
+            if (oldEffects.shouldDisableTouch() != newEffects.shouldDisableTouch()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TOUCH;
+            }
+            if (oldEffects.shouldMinimizeRadioUsage() != newEffects.shouldMinimizeRadioUsage()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_MINIMIZE_RADIO_USAGE;
+            }
+            if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) {
+                userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE;
+            }
+        }
+
+        // Since newEffects is not null, we want to adopt all the new provided device effects.
+        // Set the usermodifiedFields value separately, to reflect the updated bitmask.
+        return builder.setUserModifiedFields(userModifiedFields).build();
     }
 
     private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
@@ -992,6 +1197,7 @@
                     .setOwner(rule.component)
                     .setConfigurationActivity(rule.configurationActivity)
                     .setTriggerDescription(rule.triggerDescription)
+                    .setUserModifiedFields(rule.userModifiedFields)
                     .build();
         } else {
             azr = new AutomaticZenRule(rule.name, rule.component,
@@ -2023,6 +2229,7 @@
         if (resId == 0) {
             return null;
         }
+        Objects.requireNonNull(packageName);
         try {
             final Resources res = mPm.getResourcesForApplication(packageName);
             return res.getResourceName(resId);
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 992d8eb..dd9541e 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -175,6 +175,7 @@
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DexoptParams;
 import com.android.server.art.model.DexoptResult;
+import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.pm.Installer.LegacyDexoptDisabledException;
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.DexManager;
@@ -957,6 +958,7 @@
         final Set<String> scannedPackages = new ArraySet<>(requests.size());
         final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
         final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
+        CriticalEventLog.getInstance().logInstallPackagesStarted();
         boolean success = false;
         try {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
index 752eb53..17c901e 100644
--- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
+++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java
@@ -254,6 +254,14 @@
             String packageName = verifications.valueAt(index).second;
             AndroidPackage pkg = mConnection.getPackage(packageName);
 
+            if (pkg == null) {
+                if (DEBUG_BROADCASTS) {
+                    Slog.d(TAG,
+                            "Skip sendBroadcasts because null AndroidPackage for " + packageName);
+                }
+                continue;
+            }
+
             String hostsString = buildHostsString(pkg);
 
             Intent intent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION)
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index b0b66cf..5c86701 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -130,6 +130,28 @@
      */
     final Rect cropHint = new Rect(0, 0, 0, 0);
 
+    // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked
+    enum BindSource {
+        UNKNOWN,
+        CONNECT_LOCKED,
+        CONNECTION_TRY_TO_REBIND,
+        INITIALIZE_FALLBACK,
+        PACKAGE_UPDATE_FINISHED,
+        RESTORE_SETTINGS_LIVE_FAILURE,
+        RESTORE_SETTINGS_LIVE_SUCCESS,
+        RESTORE_SETTINGS_STATIC,
+        SET_LIVE,
+        SET_LIVE_TO_CLEAR,
+        SET_STATIC,
+        SWITCH_WALLPAPER_FAILURE,
+        SWITCH_WALLPAPER_SWITCH_USER,
+        SWITCH_WALLPAPER_UNLOCK_USER,
+    }
+
+    // Context in which this wallpaper was bound. Intended for use in resolving b/301073479 but may
+    // be useful after the issue is resolved as well.
+    BindSource mBindSource = BindSource.UNKNOWN;
+
     // map of which -> File
     private final SparseArray<File> mWallpaperFiles = new SparseArray<>();
     private final SparseArray<File> mCropFiles = new SparseArray<>();
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 5f8bbe5..de98df5 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -46,6 +46,7 @@
 import com.android.internal.util.JournaledFile;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.wallpaper.WallpaperData.BindSource;
 
 import libcore.io.IoUtils;
 
@@ -314,6 +315,14 @@
         wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
         wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
         wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f);
+        BindSource bindSource;
+        try {
+            bindSource = Enum.valueOf(BindSource.class,
+                    getAttributeString(parser, "bindSource", BindSource.UNKNOWN.name()));
+        } catch (IllegalArgumentException | NullPointerException e) {
+            bindSource = BindSource.UNKNOWN;
+        }
+        wallpaper.mBindSource = bindSource;
         int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0);
         if (dimAmountsCount > 0) {
             SparseArray<Float> allDimAmounts = new SparseArray<>(dimAmountsCount);
@@ -364,6 +373,11 @@
         return parser.getAttributeFloat(null, name, defValue);
     }
 
+    private String getAttributeString(XmlPullParser parser, String name, String defValue) {
+        String s = parser.getAttributeValue(null, name);
+        return (s != null) ? s : defValue;
+    }
+
     void saveSettingsLocked(int userId, WallpaperData wallpaper, WallpaperData lockWallpaper) {
         JournaledFile journal = makeJournaledFile(userId);
         FileOutputStream fstream = null;
@@ -423,6 +437,7 @@
         }
 
         out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount);
+        out.attribute(null, "bindSource", wallpaper.mBindSource.name());
         int dimAmountsCount = wallpaper.mUidToDimAmount.size();
         out.attributeInt(null, "dimAmountsCount", dimAmountsCount);
         if (dimAmountsCount > 0) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 1485b96..3782b42 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -122,6 +122,7 @@
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.utils.TimingsTraceAndSlog;
+import com.android.server.wallpaper.WallpaperData.BindSource;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
@@ -335,6 +336,7 @@
                     };
 
                     // If this was the system wallpaper, rebind...
+                    wallpaper.mBindSource = BindSource.SET_STATIC;
                     bindWallpaperComponentLocked(mImageWallpaper, true, false, wallpaper,
                             callback);
                 }
@@ -354,6 +356,7 @@
                         }
                     };
 
+                    wallpaper.mBindSource = BindSource.SET_STATIC;
                     bindWallpaperComponentLocked(mImageWallpaper, true /* force */,
                             false /* fromUser */, wallpaper, callback);
                 } else if (isAppliedToLock) {
@@ -811,6 +814,7 @@
                 Slog.w(TAG, "Failed attaching wallpaper on display", e);
                 if (wallpaper != null && !wallpaper.wallpaperUpdating
                         && connection.getConnectedEngineSize() == 0) {
+                    wallpaper.mBindSource = BindSource.CONNECT_LOCKED;
                     bindWallpaperComponentLocked(null /* componentName */, false /* force */,
                             false /* fromUser */, wallpaper, null /* reply */);
                 }
@@ -1035,6 +1039,7 @@
                 final ComponentName wpService = mWallpaper.wallpaperComponent;
                 // The broadcast of package update could be delayed after service disconnected. Try
                 // to re-bind the service for 10 seconds.
+                mWallpaper.mBindSource = BindSource.CONNECTION_TRY_TO_REBIND;
                 if (bindWallpaperComponentLocked(
                         wpService, true, false, mWallpaper, null)) {
                     mWallpaper.connection.scheduleTimeoutLocked();
@@ -1321,6 +1326,7 @@
                         }
                         wallpaper.wallpaperUpdating = false;
                         clearWallpaperComponentLocked(wallpaper);
+                        wallpaper.mBindSource = BindSource.PACKAGE_UPDATE_FINISHED;
                         if (!bindWallpaperComponentLocked(wpService, false, false,
                                 wallpaper, null)) {
                             Slog.w(TAG, "Wallpaper " + wpService
@@ -1711,6 +1717,7 @@
                 if (mHomeWallpaperWaitingForUnlock) {
                     final WallpaperData systemWallpaper =
                             getWallpaperSafeLocked(userId, FLAG_SYSTEM);
+                    systemWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
                     switchWallpaper(systemWallpaper, null);
                     // TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper
                     notifyCallbacksLocked(systemWallpaper);
@@ -1718,6 +1725,7 @@
                 if (mLockWallpaperWaitingForUnlock) {
                     final WallpaperData lockWallpaper =
                             getWallpaperSafeLocked(userId, FLAG_LOCK);
+                    lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER;
                     switchWallpaper(lockWallpaper, null);
                     notifyCallbacksLocked(lockWallpaper);
                 }
@@ -1838,6 +1846,7 @@
         // delete them in order to show the default wallpaper.
         clearWallpaperBitmaps(wallpaper);
 
+        fallback.mBindSource = BindSource.SWITCH_WALLPAPER_FAILURE;
         bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply);
         if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true;
         if ((wallpaper.mWhich & FLAG_LOCK) != 0) mLockWallpaperWaitingForUnlock = true;
@@ -2963,6 +2972,8 @@
                  */
                 boolean forceRebind = force || (same && systemIsBoth && which == FLAG_SYSTEM);
 
+                newWallpaper.mBindSource =
+                        (name == null) ? BindSource.SET_LIVE_TO_CLEAR : BindSource.SET_LIVE;
                 bindSuccess = bindWallpaperComponentLocked(name, /* force */
                         forceRebind, /* fromUser */ true, newWallpaper, reply);
                 if (bindSuccess) {
@@ -3530,6 +3541,7 @@
             mFallbackWallpaper = new WallpaperData(systemUserId, FLAG_SYSTEM);
             mFallbackWallpaper.allowBackup = false;
             mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked();
+            mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK;
             bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false,
                     mFallbackWallpaper, null);
         }
@@ -3553,11 +3565,13 @@
             wallpaper.allowBackup = true;   // by definition if it was restored
             if (wallpaper.nextWallpaperComponent != null
                     && !wallpaper.nextWallpaperComponent.equals(mImageWallpaper)) {
+                wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_SUCCESS;
                 if (!bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, false, false,
                         wallpaper, null)) {
                     // No such live wallpaper or other failure; fall back to the default
                     // live wallpaper (since the profile being restored indicated that the
                     // user had selected a live rather than static one).
+                    wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_FAILURE;
                     bindWallpaperComponentLocked(null, false, false, wallpaper, null);
                 }
                 success = true;
@@ -3575,6 +3589,7 @@
                         + " id=" + wallpaper.wallpaperId);
                 if (success) {
                     mWallpaperCropper.generateCrop(wallpaper); // based on the new image + metadata
+                    wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_STATIC;
                     bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, true, false,
                             wallpaper, null);
                 }
@@ -3608,7 +3623,8 @@
         pw.print(" User "); pw.print(wallpaper.userId);
         pw.print(": id="); pw.print(wallpaper.wallpaperId);
         pw.print(": mWhich="); pw.print(wallpaper.mWhich);
-        pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth);
+        pw.print(": mSystemWasBoth="); pw.print(wallpaper.mSystemWasBoth);
+        pw.print(": mBindSource="); pw.println(wallpaper.mBindSource.name());
         pw.println(" Display state:");
         mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
             pw.print("  displayId=");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5fa2610..3a792d0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2495,7 +2495,14 @@
 
         ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData");
         mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams);
-        if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+        if ((!mStyleFillsParent && task.getChildCount() > 1)
+                || task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+            // Case 1:
+            // If it is moving a Task{[0]=main activity, [1]=translucent activity} to front, use
+            // shared starting window so that the transition doesn't need to wait for the activity
+            // behind the translucent activity. Also, onFirstWindowDrawn will check all visible
+            // activities are drawn in the task to remove the snapshot starting window.
+            // Case 2:
             // Associate with the task so if this activity is resized by task fragment later, the
             // starting window can keep the same bounds as the task.
             associateStartingDataWithTask();
@@ -10616,6 +10623,14 @@
 
     @Override
     boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) {
+        if (task != null && task.mSharedStartingData != null) {
+            final WindowState startingWin = task.topStartingWindow();
+            if (startingWin != null && startingWin.mSyncState == SYNC_STATE_READY
+                    && mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
+                // The sync is ready if a drawn starting window covered the task.
+                return true;
+            }
+        }
         if (!super.isSyncFinished(group)) return false;
         if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController
                 .isVisibilityUnknown(this)) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 03e263a..a7a6bf2 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1411,12 +1411,13 @@
         return isUidPresent;
     }
 
+    WindowState topStartingWindow() {
+        return getWindow(w -> w.mAttrs.type == TYPE_APPLICATION_STARTING);
+    }
+
     ActivityRecord topActivityContainsStartingWindow() {
-        if (getParent() == null) {
-            return null;
-        }
-        return getActivity((r) -> r.getWindow(window ->
-                window.getBaseType() == TYPE_APPLICATION_STARTING) != null);
+        final WindowState startingWindow = topStartingWindow();
+        return startingWindow != null ? startingWindow.mActivityRecord : null;
     }
 
     /**
@@ -3698,6 +3699,16 @@
                 }
                 wc.assignLayer(t, layer++);
 
+                // Boost the adjacent TaskFragment for dimmer if needed.
+                final TaskFragment taskFragment = wc.asTaskFragment();
+                if (taskFragment != null && taskFragment.isEmbedded()
+                        && taskFragment.isVisibleRequested()) {
+                    final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
+                    if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
+                        adjacentTf.assignLayer(t, layer++);
+                    }
+                }
+
                 // Place the decor surface just above the owner TaskFragment.
                 if (mDecorSurfaceContainer != null && !decorSurfacePlaced
                         && wc == mDecorSurfaceContainer.mOwnerTaskFragment) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index f51bd1b..f56759f 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -39,6 +39,7 @@
 import static android.os.Process.SYSTEM_UID;
 import static android.os.UserHandle.USER_NULL;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_NONE;
@@ -2995,6 +2996,30 @@
         }, false /* traverseTopToBottom */);
     }
 
+    boolean shouldBoostDimmer() {
+        if (asTask() != null || !isDimmingOnParentTask()) {
+            // early return if not embedded or should not dim on parent Task.
+            return false;
+        }
+
+        final TaskFragment adjacentTf = getAdjacentTaskFragment();
+        if (adjacentTf == null) {
+            // early return if no adjacent TF.
+            return false;
+        }
+
+        if (getParent().mChildren.indexOf(adjacentTf) < getParent().mChildren.indexOf(this)) {
+            // early return if this TF already has higher z-ordering.
+            return false;
+        }
+
+        // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
+        return forAllWindows(
+                (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
+                        && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
+                        || w.mActivityRecord.isVisible()), true);
+    }
+
     @Override
     Dimmer getDimmer() {
         // If this is in an embedded TaskFragment and we want the dim applies on the TaskFragment.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5e8d502..c63cc43 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1803,7 +1803,12 @@
 
             // Don't do layout here, the window must call
             // relayout to be displayed, so we'll do it there.
-            win.getParent().assignChildLayers();
+            if (win.mActivityRecord != null && win.mActivityRecord.isEmbedded()) {
+                // Assign child layers from the parent Task if the Activity is embedded.
+                win.getTask().assignChildLayers();
+            } else {
+                win.getParent().assignChildLayers();
+            }
 
             if (focusChanged) {
                 displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
index eb6e8b4..ad4d91f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
@@ -96,6 +96,8 @@
                 .append(displayBrightnessState.isSlowChange())
                 .append("\n    maxBrightness:")
                 .append(displayBrightnessState.getMaxBrightness())
+                .append("\n    minBrightness:")
+                .append(displayBrightnessState.getMinBrightness())
                 .append("\n    customAnimationRate:")
                 .append(displayBrightnessState.getCustomAnimationRate())
                 .append("\n    shouldUpdateScreenBrightnessSetting:")
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
index dafbbb3..33d3020 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
@@ -16,16 +16,24 @@
 
 package com.android.server.display
 
+import android.content.Context
+import android.os.Looper
 import android.view.Display
 import androidx.test.filters.SmallTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.junit.MockitoJUnit
 
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 import java.util.concurrent.Executor
 
 @SmallTest
@@ -39,11 +47,16 @@
     private val mockBlanker = mock<DisplayBlanker>()
     private val mockColorFade = mock<ColorFade>()
     private val mockExecutor = mock<Executor>()
+    private val mockContext = mock<Context>()
 
     @Before
     fun setUp() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare()
+        }
         displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON,
                 mockExecutor)
+        whenever(mockColorFade.prepare(eq(mockContext), anyInt())).thenReturn(true)
     }
 
     @Test
@@ -56,4 +69,31 @@
 
         verify(mockColorFade).destroy()
     }
+
+    @Test
+    fun `GIVEN not prepared WHEN draw runnable is called THEN colorFade not drawn`() {
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade, never()).draw(anyFloat())
+    }
+    @Test
+    fun `GIVEN prepared WHEN draw runnable is called THEN colorFade is drawn`() {
+        displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE)
+        clearInvocations(mockColorFade)
+
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade).draw(anyFloat())
+    }
+
+    @Test
+    fun `GIVEN prepared AND stopped WHEN draw runnable is called THEN colorFade is not drawn`() {
+        displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE)
+        clearInvocations(mockColorFade)
+        displayPowerState.stop()
+
+        displayPowerState.mColorFadeDrawRunnable.run()
+
+        verify(mockColorFade, never()).draw(anyFloat())
+    }
 }
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 00f9892..c92ce25 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -176,6 +176,10 @@
         when(mockArray.length()).thenReturn(0);
         when(mMockedResources.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray))
                 .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_displayCutoutSideOverrideArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride))
+                .thenReturn(new String[]{});
         when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray))
                 .thenReturn(mockArray);
         when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
index e58b3e8..990c383 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
@@ -58,9 +58,14 @@
 
     @Test
     public void setModifierDoesntSetIfModifierIsBeyondExtremes() {
-        int extremeModifier = 0x16;
+        int extremeModifier = 0x40; // equal to BrightnessReason.MODIFIER_MASK * 2
+
+        // reset modifier
+        mBrightnessReason.setModifier(0);
+
+        // test extreme
         mBrightnessReason.setModifier(extremeModifier);
-        assertEquals(mBrightnessReason.getModifier(), BrightnessReason.MODIFIER_LOW_POWER);
+        assertEquals(0, mBrightnessReason.getModifier());
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index 6ba7368..5294943 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -29,8 +29,10 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.provider.DeviceConfig;
+import android.testing.TestableContext;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.brightness.BrightnessReason;
@@ -39,6 +41,7 @@
 import com.android.server.testutils.TestHandler;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -52,14 +55,15 @@
 
     private final TestHandler mTestHandler = new TestHandler(null);
 
+    @Rule
+    public final TestableContext mMockContext = new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getContext());
     @Mock
     private BrightnessClamperController.ClamperChangeListener mMockExternalListener;
 
     @Mock
     private BrightnessClamperController.DisplayDeviceData mMockDisplayDeviceData;
     @Mock
-    private Context mMockContext;
-    @Mock
     private DeviceConfigParameterProvider mMockDeviceConfigParameterProvider;
     @Mock
     private BrightnessClamper<BrightnessClamperController.DisplayDeviceData> mMockClamper;
@@ -231,6 +235,13 @@
         assertEquals(initialSlowChange, state.isSlowChange());
     }
 
+    @Test
+    public void testStop() {
+        mClamperController.stop();
+        verify(mMockModifier).stop();
+        verify(mMockClamper).stop();
+    }
+
     private BrightnessClamperController createBrightnessClamperController() {
         return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener,
                 mMockDisplayDeviceData, mMockContext, mFlags);
@@ -240,14 +251,14 @@
 
         private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
                 mClampers;
-        private final List<BrightnessModifier> mModifiers;
+        private final List<BrightnessStateModifier> mModifiers;
 
         private BrightnessClamperController.ClamperChangeListener mCapturedChangeListener;
 
         private TestInjector(
                 List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
                         clampers,
-                List<BrightnessModifier> modifiers) {
+                List<BrightnessStateModifier> modifiers) {
             mClampers = clampers;
             mModifiers = modifiers;
         }
@@ -268,7 +279,8 @@
         }
 
         @Override
-        List<BrightnessModifier> getModifiers(Context context) {
+        List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
+                Handler handler, BrightnessClamperController.ClamperChangeListener listener) {
             return mModifiers;
         }
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
new file mode 100644
index 0000000..ac7d1f5
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.display.brightness.clamper
+
+import android.os.PowerManager
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.TestableContext
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.display.brightness.BrightnessReason
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+private const val userId = UserHandle.USER_CURRENT
+
+class BrightnessLowLuxModifierTest {
+
+    private var mockClamperChangeListener =
+            mock<BrightnessClamperController.ClamperChangeListener>()
+
+    val context = TestableContext(
+            InstrumentationRegistry.getInstrumentation().getContext())
+
+    private val testHandler = TestHandler(null)
+    private lateinit var modifier: BrightnessLowLuxModifier
+
+    @Before
+    fun setUp() {
+        modifier = BrightnessLowLuxModifier(testHandler, mockClamperChangeListener, context)
+        testHandler.flush()
+    }
+
+    @Test
+    fun testThrottlingBounds() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // true
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // TODO: code currently returns MIN/MAX; update with lux values
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+    }
+
+    @Test
+    fun testGetReason_UserSet() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // Test restriction from user setting
+        assertThat(modifier.brightnessReason)
+                .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND)
+    }
+
+    @Test
+    fun testGetReason_Lux() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
+        Settings.Secure.putFloatForUser(context.contentResolver,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
+        modifier.recalculateLowerBound()
+        testHandler.flush()
+        assertThat(modifier.isActive).isTrue()
+
+        // Test restriction from lux setting
+        assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
index 999e33c..3d8ec2e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -18,19 +18,31 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.Flags;
 import android.os.Parcel;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenDeviceEffects;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
 public class ZenDeviceEffectsTest extends UiServiceTestCase {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void builder() {
         ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
@@ -40,6 +52,7 @@
                 .setShouldMaximizeDoze(true)
                 .setShouldUseNightMode(false)
                 .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true)
+                .setUserModifiedFields(8)
                 .build();
 
         assertThat(deviceEffects.shouldDimWallpaper()).isTrue();
@@ -52,6 +65,7 @@
         assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse();
         assertThat(deviceEffects.shouldUseNightMode()).isFalse();
         assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8);
     }
 
     @Test
@@ -83,6 +97,7 @@
                 .setShouldMinimizeRadioUsage(true)
                 .setShouldUseNightMode(true)
                 .setShouldSuppressAmbientDisplay(true)
+                .setUserModifiedFields(6)
                 .build();
 
         Parcel parcel = Parcel.obtain();
@@ -101,6 +116,7 @@
         assertThat(copy.shouldUseNightMode()).isTrue();
         assertThat(copy.shouldSuppressAmbientDisplay()).isTrue();
         assertThat(copy.shouldDisplayGrayscale()).isFalse();
+        assertThat(copy.getUserModifiedFields()).isEqualTo(6);
     }
 
     @Test
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 3185c50..177d645 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -19,12 +19,15 @@
 import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 
+import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.app.NotificationManager.Policy;
 import android.content.ComponentName;
@@ -46,6 +49,7 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -83,6 +87,11 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void testPriorityOnlyMutingAllNotifications() {
         ZenModeConfig config = getMutedRingerConfig();
@@ -275,6 +284,7 @@
         assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders());
         assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders());
         assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels());
+        assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields());
     }
 
     @Test
@@ -327,6 +337,53 @@
     }
 
     @Test
+    public void testCanBeUpdatedByApp_nullPolicyAndDeviceEffects() throws Exception {
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenPolicy = null;
+        rule.zenDeviceEffects = null;
+
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        rule.userModifiedFields = 1;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
+    public void testCanBeUpdatedByApp_policyModified() throws Exception {
+        ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
+        ZenPolicy policy = policyBuilder.build();
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenPolicy = policy;
+
+        assertThat(rule.userModifiedFields).isEqualTo(0);
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        policy = policyBuilder.setUserModifiedFields(1).build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(1);
+        rule.zenPolicy = policy;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
+    public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
+        ZenDeviceEffects.Builder deviceEffectsBuilder =
+                new ZenDeviceEffects.Builder().setUserModifiedFields(0);
+        ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.zenDeviceEffects = deviceEffects;
+
+        assertThat(rule.userModifiedFields).isEqualTo(0);
+        assertThat(rule.canBeUpdatedByApp()).isTrue();
+
+        deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
+        assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
+        rule.zenDeviceEffects = deviceEffects;
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
     public void testWriteToParcel() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
@@ -347,6 +404,7 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = 16;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
 
@@ -371,6 +429,7 @@
         assertEquals(rule.allowManualInvocation, parceled.allowManualInvocation);
         assertEquals(rule.iconResName, parceled.iconResName);
         assertEquals(rule.type, parceled.type);
+        assertEquals(rule.userModifiedFields, parceled.userModifiedFields);
         assertEquals(rule.triggerDescription, parceled.triggerDescription);
         assertEquals(rule.zenPolicy, parceled.zenPolicy);
         assertEquals(rule, parceled);
@@ -448,6 +507,7 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = 4;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
 
@@ -476,6 +536,7 @@
 
         assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation);
         assertEquals(rule.type, fromXml.type);
+        assertEquals(rule.userModifiedFields, fromXml.userModifiedFields);
         assertEquals(rule.triggerDescription, fromXml.triggerDescription);
         assertEquals(rule.iconResName, fromXml.iconResName);
     }
@@ -550,6 +611,22 @@
     }
 
     @Test
+    public void testRuleXml_userModifiedField() throws Exception {
+        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+        assertThat(rule.userModifiedFields).isEqualTo(1);
+        assertThat(rule.canBeUpdatedByApp()).isFalse();
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeRuleXml(rule, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
+
+        assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields);
+        assertThat(fromXml.canBeUpdatedByApp()).isFalse();
+    }
+
+    @Test
     public void testZenPolicyXml_classic() throws Exception {
         ZenPolicy policy = new ZenPolicy.Builder()
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
@@ -615,6 +692,7 @@
                 .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
                 .hideAllVisualEffects()
                 .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true)
+                .setUserModifiedFields(4)
                 .build();
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -649,6 +727,7 @@
         assertEquals(policy.getVisualEffectAmbient(), fromXml.getVisualEffectAmbient());
         assertEquals(policy.getVisualEffectNotificationList(),
                 fromXml.getVisualEffectNotificationList());
+        assertEquals(policy.getUserModifiedFields(), fromXml.getUserModifiedFields());
     }
 
     private ZenModeConfig getMutedRingerConfig() {
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 93cd44e..7e92e42 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -76,7 +76,7 @@
                     ? Set.of()
                     : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
                             RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
-                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS);
+                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_USER_MODIFIED_FIELDS);
 
     // allowPriorityChannels is flagged by android.app.modes_api
     public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
@@ -304,6 +304,7 @@
             rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
                     .setShouldDimWallpaper(true)
                     .build();
+            rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         }
         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 f84d8e9..9eed974 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -21,6 +21,9 @@
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
@@ -2197,7 +2200,7 @@
     }
 
     @Test
-    public void addAutomaticZenRule_fromUser_respectsHiddenEffects() {
+    public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
@@ -2222,7 +2225,13 @@
                 "reasons", 0);
 
         AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
-        assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+
+        // savedRule.getDeviceEffects() is equal to zde, except for the userModifiedFields.
+        // So we clear before comparing.
+        ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
+                .setUserModifiedFields(0).build();
+
+        assertThat(savedEffects).isEqualTo(zde);
     }
 
     @Test
@@ -2298,8 +2307,11 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0);
 
         ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder()
-                .setShouldUseNightMode(true) // Good
-                .setShouldMaximizeDoze(true) // Also good
+                .setShouldUseNightMode(true)
+                .setShouldMaximizeDoze(true)
+                // Just to emphasize that unset values default to false;
+                // even with this line removed, tap to wake would be set to false.
+                .setShouldDisableTapToWake(false)
                 .build();
         mZenModeHelper.updateAutomaticZenRule(ruleId,
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
@@ -2308,7 +2320,13 @@
                 UPDATE_ORIGIN_USER, "reasons", 0);
 
         AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
-        assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
+
+        // savedRule.getDeviceEffects() is equal to updateFromUser, except for the
+        // userModifiedFields, so we clear before comparing.
+        ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
+                .setUserModifiedFields(0).build();
+
+        assertThat(savedEffects).isEqualTo(updateFromUser);
     }
 
     @Test
@@ -3321,6 +3339,7 @@
 
         rule.allowManualInvocation = ALLOW_MANUAL;
         rule.type = TYPE;
+        rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
 
@@ -3335,6 +3354,7 @@
         assertEquals(POLICY, actual.getZenPolicy());
         assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity());
         assertEquals(TYPE, actual.getType());
+        assertEquals(AutomaticZenRule.FIELD_NAME, actual.getUserModifiedFields());
         assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed());
         assertEquals(CREATION_TIME, actual.getCreationTime());
         assertEquals(OWNER.getPackageName(), actual.getPackageName());
@@ -3376,10 +3396,480 @@
         assertEquals(ALLOW_MANUAL, rule.allowManualInvocation);
         assertEquals(OWNER.getPackageName(), rule.getPkg());
         assertEquals(ICON_RES_NAME, rule.iconResName);
+        // Because the origin of the update is the app, we don't expect the bitmask to change.
+        assertEquals(0, rule.userModifiedFields);
         assertEquals(TRIGGER_DESC, rule.triggerDescription);
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() {
+        // Add a starting rule with the name OriginalName.
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Checks the name can be changed by the app because the user has not modified it.
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("NewName");
+        assertThat(rule.canUpdate()).isTrue();
+
+        // The user modifies some other field in the rule, which makes the rule as a whole not
+        // app modifiable.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getUserModifiedFields())
+                .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+        assertThat(rule.canUpdate()).isFalse();
+
+        // ...but the app can still modify the name, because the name itself hasn't been modified
+        // by the user.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewAppName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("NewAppName");
+
+        // The user modifies the name.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("UserProvidedName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("UserProvidedName");
+        assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME
+                | AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+
+        // The app is no longer able to modify the name.
+        azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setName("NewAppName")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.getName()).isEqualTo("UserProvidedName");
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(new ZenPolicy.Builder().build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Modifies the zen policy and device effects
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .build();
+        ZenDeviceEffects deviceEffects =
+                new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Update the rule with the AZR from origin user.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_USER should change the bitmask and change the values.
+        assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+        assertThat(rule.getUserModifiedFields())
+                .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+        assertThat(rule.getZenPolicy().getUserModifiedFields())
+                .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS);
+        assertThat(rule.getZenPolicy().getAllowedChannels())
+                .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(rule.getDeviceEffects().getUserModifiedFields())
+                .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_doesNotUpdateValuesForInitUserOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL) // Already the default, no change
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the user, to set user-modified bits.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME);
+
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Attempts to update the rule with the AZR from origin init user.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason",
+                Process.SYSTEM_UID);
+        AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_INIT_USER does not change the bitmask or values if rule is user modified.
+        // TODO: b/318506692 - Remove once we check that INIT origins can't call add/updateAZR.
+        assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
+        assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
+        assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                rule.getZenPolicy().getUserModifiedFields());
+        assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+        assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                rule.getDeviceEffects().getUserModifiedFields());
+        assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
+
+        // Creates a new rule with the AZR from origin init user.
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule newRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+
+        // UPDATE_ORIGIN_INIT_USER does change the values if the rule is new,
+        // but does not update the bitmask.
+        assertThat(newRule.getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+        assertThat(newRule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(newRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
+        assertThat(newRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // Modifies the zen policy and device effects
+        ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects =
+                new ZenDeviceEffects.Builder(rule.getDeviceEffects())
+                        .setShouldDisplayGrayscale(true)
+                        .build();
+        AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Update the rule with the AZR from origin systemUI.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                "reason", Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask.
+        assertThat(rule.getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(false)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(false)
+                        .build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        ZenPolicy policy = new ZenPolicy.Builder()
+                .allowReminders(true)
+                .build();
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azrUpdate =  new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(policy)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Since the rule is not already user modified, UPDATE_ORIGIN_UNKNOWN can modify the rule.
+        // The bitmask is not modified.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
+        assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+        assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                rule.getZenPolicy().getUserModifiedFields());
+        assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                rule.getDeviceEffects().getUserModifiedFields());
+        assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+
+        // Creates another rule, this time from user. This will have user modified bits set.
+        String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
+        assertThat(ruleUser.canUpdate()).isFalse();
+
+        // Zen rule update coming from unknown origin. This cannot fully update the rule, because
+        // the rule is already considered user modified.
+        mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN,
+                "reason", Process.SYSTEM_UID);
+        ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
+
+        // UPDATE_ORIGIN_UNKNOWN can only change the value if the rule is not already user modified,
+        // so the rule is not changed, and neither is the bitmask.
+        assertThat(ruleUser.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
+        // Interruption Filter All is the default value, so it's not included as a modified field.
+        assertThat(ruleUser.getUserModifiedFields() | AutomaticZenRule.FIELD_NAME).isGreaterThan(0);
+        assertThat(ruleUser.getZenPolicy().getUserModifiedFields()
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS).isGreaterThan(0);
+        assertThat(ruleUser.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(ruleUser.getDeviceEffects().getUserModifiedFields()
+                | ZenDeviceEffects.FIELD_GRAYSCALE).isGreaterThan(0);
+        assertThat(ruleUser.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowReminders(true)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .build())
+                .build();
+        // Adds the rule using origin unknown, to show that a new rule is always allowed.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_UNKNOWN, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // The values are modified but the bitmask is not.
+        assertThat(rule.canUpdate()).isTrue();
+        assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullDeviceEffectsUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                // Sets Device Effects to null
+                .setDeviceEffects(null)
+                .build();
+
+        // Zen rule update coming from unknown origin, but since the rule isn't already
+        // user modified, it can be updated.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // When AZR's ZenDeviceEffects is null, the updated rule's device effects will be null.
+        assertThat(rule.getDeviceEffects()).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullPolicyUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(new ZenPolicy.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                // Set zen policy to null
+                .setZenPolicy(null)
+                .build();
+
+        // Zen rule update coming from unknown origin, but since the rule isn't already
+        // user modified, it can be updated.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null.
+        assertThat(rule.getZenPolicy()).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() {
+        when(mContext.checkCallingPermission(anyString()))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setZenPolicy(null)
+                // .setDeviceEffects(new ZenDeviceEffects.Builder().build())
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        // Create a fully populated ZenPolicy.
+        ZenPolicy policy = new ZenPolicy.Builder()
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default
+                .allowReminders(true) // Differs from the default
+                .allowEvents(true) // Differs from the default
+                .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
+                .allowMessages(PEOPLE_TYPE_STARRED)
+                .allowCalls(PEOPLE_TYPE_STARRED)
+                .allowRepeatCallers(true)
+                .allowAlarms(true)
+                .allowMedia(true)
+                .allowSystem(true) // Differs from the default
+                .showFullScreenIntent(true) // Differs from the default
+                .showLights(true) // Differs from the default
+                .showPeeking(true) // Differs from the default
+                .showStatusBarIcons(true)
+                .showBadges(true)
+                .showInAmbientDisplay(true) // Differs from the default
+                .showInNotificationList(true)
+                .build();
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
+                .setZenPolicy(policy)
+                .build();
+
+        // Applies the update to the rule.
+        // Default config defined in getDefaultConfigParser() is used as the original rule.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // New ZenPolicy differs from the default config
+        assertThat(rule.getZenPolicy()).isNotNull();
+        assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+                ZenPolicy.FIELD_ALLOW_CHANNELS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
+                | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
+                | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
+                | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
+                | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
+                | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
+        );
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void automaticZenRuleToZenRule_nullToNonNullDeviceEffectsUpdate() {
+        // Adds a starting rule with empty zen policies and device effects
+        AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
+                .setDeviceEffects(null)
+                .build();
+        // Adds the rule using the app, to avoid having any user modified bits set.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(rule.canUpdate()).isTrue();
+
+        ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .build();
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(rule)
+                .setDeviceEffects(deviceEffects)
+                .build();
+
+        // Applies the update to the rule.
+        mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason",
+                Process.SYSTEM_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+
+        // New ZenDeviceEffects is used; all fields considered set, since previously were null.
+        assertThat(rule.getDeviceEffects()).isNotNull();
+        assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+        assertThat(rule.canUpdate()).isFalse();
+        assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+                ZenDeviceEffects.FIELD_GRAYSCALE);
+    }
+
+    @Test
     public void testUpdateAutomaticRule_disabled_triggersBroadcast() throws Exception {
         setupZenConfig();
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 2f4f891c..21c96d6 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -34,6 +34,7 @@
 
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,6 +50,11 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Before
+    public final void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+    }
+
     @Test
     public void testZenPolicyApplyAllowedToDisallowed() {
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
@@ -640,6 +646,54 @@
     }
 
     @Test
+    public void testFromParcel() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        builder.setUserModifiedFields(10);
+
+        ZenPolicy policy = builder.build();
+        assertThat(policy.getUserModifiedFields()).isEqualTo(10);
+
+        Parcel parcel = Parcel.obtain();
+        policy.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel);
+        assertThat(fromParcel.getUserModifiedFields()).isEqualTo(10);
+    }
+
+    @Test
+    public void testPolicy_userModifiedFields() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        builder.setUserModifiedFields(10);
+        assertThat(builder.build().getUserModifiedFields()).isEqualTo(10);
+
+        builder.setUserModifiedFields(0);
+        assertThat(builder.build().getUserModifiedFields()).isEqualTo(0);
+    }
+
+    @Test
+    public void testPolicyBuilder_constructFromPolicy() {
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false)
+                .showLights(true).showBadges(false)
+                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .setUserModifiedFields(20).build();
+
+        ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build();
+
+        assertThat(newPolicy.getPriorityCategoryAlarms()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(newPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(newPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW);
+
+        assertThat(newPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
+
+        assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20);
+    }
+
+    @Test
     public void testTooLongLists_fromParcel() {
         ArrayList<Integer> longList = new ArrayList<Integer>(50);
         for (int i = 0; i < 50; i++) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index be30593..6c5f975 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -145,6 +146,25 @@
     }
 
     @Test
+    public void testFinishSyncByStartingWindow() {
+        final ActivityRecord taskRoot = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task task = taskRoot.getTask();
+        final ActivityRecord translucentTop = new ActivityBuilder(mAtm).setTask(task)
+                .setActivityTheme(android.R.style.Theme_Translucent).build();
+        createWindow(null, TYPE_BASE_APPLICATION, taskRoot, "win");
+        final WindowState startingWindow = createWindow(null, TYPE_APPLICATION_STARTING,
+                translucentTop, "starting");
+        startingWindow.mStartingData = new SnapshotStartingData(mWm, null, 0);
+        task.mSharedStartingData = startingWindow.mStartingData;
+        task.prepareSync();
+
+        final BLASTSyncEngine.SyncGroup group = mock(BLASTSyncEngine.SyncGroup.class);
+        assertFalse(task.isSyncFinished(group));
+        startingWindow.onSyncFinishedDrawing();
+        assertTrue(task.isSyncFinished(group));
+    }
+
+    @Test
     public void testInvisibleSyncCallback() {
         TestWindowContainer mockWC = new TestWindowContainer(mWm, true /* waiter */);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 45e1e95..b360800 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -43,6 +43,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
+import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK;
 import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -1620,6 +1621,29 @@
     }
 
     @Test
+    public void testBoostDimmingTaskFragmentOnTask() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment primary = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment secondary = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+
+        primary.mVisibleRequested = true;
+        secondary.mVisibleRequested = true;
+        primary.setAdjacentTaskFragment(secondary);
+        secondary.setAdjacentTaskFragment(primary);
+        primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
+        doReturn(true).when(primary).shouldBoostDimmer();
+        task.assignChildLayers(t);
+
+        // The layers are initially assigned via the hierarchy, but the primary will be boosted and
+        // assigned again to above of the secondary.
+        verify(primary).assignLayer(t, 0);
+        verify(secondary).assignLayer(t, 1);
+        verify(primary).assignLayer(t, 2);
+    }
+
+    @Test
     public void testMoveOrCreateDecorSurface() {
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final Task task =  new TaskBuilder(mSupervisor).setCreateActivity(true).build();
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 84777c9..9b5ee0c 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3055,6 +3055,29 @@
     boolean setEmergencyCallToSatelliteHandoverType(int handoverType, int delaySeconds);
 
     /**
+     * This API should be used by only CTS tests to forcefully set the country codes.
+     *
+     * @param reset {@code true} mean the overridden country codes should not be used, {@code false}
+     *              otherwise.
+     * @return {@code true} if the country code is set successfully, {@code false} otherwise.
+     */
+    boolean setCountryCodes(in boolean reset, in List<String> currentNetworkCountryCodes,
+            in Map cachedNetworkCountryCodes, in String locationCountryCode,
+            in long locationCountryCodeTimestampNanos);
+
+    /**
+     * This API should be used by only CTS tests to override the overlay configs of satellite
+     * access controller.
+     *
+     * @param reset {@code true} mean the overridden configs should not be used, {@code false}
+     *              otherwise.
+     * @return {@code true} if the overlay configs are set successfully, {@code false} otherwise.
+     */
+    boolean setSatelliteAccessControlOverlayConfigs(in boolean reset, in boolean isAllowed,
+            in String s2CellFile, in long locationFreshDurationNanos,
+            in List<String> satelliteCountryCodes);
+
+    /**
      * Test method to confirm the file contents are not altered.
      */
      @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index ba9e4a8..f82d9ca 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -130,14 +130,13 @@
     private static final Pattern PATTERN_SYSTEM_FONT_FILES =
             Pattern.compile("^/(system|product)/fonts/");
 
-    private String mKeyId;
     private FontManager mFontManager;
     private UiDevice mUiDevice;
 
     @Before
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        mKeyId = insertCert(CERT_PATH);
+        insertCert(CERT_PATH);
         mFontManager = context.getSystemService(FontManager.class);
         expectCommandToSucceed("cmd font clear");
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
@@ -147,9 +146,6 @@
     public void tearDown() throws Exception {
         // Ignore errors because this may fail if updatable system font is not enabled.
         runShellCommand("cmd font clear", null);
-        if (mKeyId != null) {
-            expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
-        }
     }
 
     @Test
@@ -369,20 +365,11 @@
         assertThat(isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID)).isFalse();
     }
 
-    private static String insertCert(String certPath) throws Exception {
-        Pair<String, String> result;
-        try (InputStream is = new FileInputStream(certPath)) {
-            result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
-        }
+    private static void insertCert(String certPath) throws Exception {
         // /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts
         final String copiedCert = "/data/fonts/debug_cert.der";
         runShellCommand("cp " + certPath + " " + copiedCert, null);
         runShellCommand("cmd font install-debug-cert " + copiedCert, null);
-        // Assert that there are no errors.
-        assertThat(result.second).isEmpty();
-        String keyId = result.first.trim();
-        assertThat(keyId).matches("^\\d+$");
-        return keyId;
     }
 
     private int updateFontFile(String fontPath, String signaturePath) throws IOException {