Merge "Refactor the VDM locks" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index e5c059e..8b95679 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -230,6 +230,17 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "telephony_flags_core_java_exported_lib",
+    aconfig_declarations: "telephony_flags",
+    mode: "exported",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.wifi",
+    ],
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 cc_aconfig_library {
     name: "telephony_flags_c_lib",
     aconfig_declarations: "telephony_flags",
diff --git a/core/api/current.txt b/core/api/current.txt
index dc17fe7..5c0ecf72 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -100,6 +100,9 @@
     field public static final String EXECUTE_APP_ACTION = "android.permission.EXECUTE_APP_ACTION";
     field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS = "android.permission.EXECUTE_APP_FUNCTIONS";
     field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_COARSE = "android.permission.EYE_TRACKING_COARSE";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_FINE = "android.permission.EYE_TRACKING_FINE";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING = "android.permission.FACE_TRACKING";
     field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST";
     field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE";
     field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA";
@@ -120,6 +123,8 @@
     field public static final String GET_PACKAGE_SIZE = "android.permission.GET_PACKAGE_SIZE";
     field @Deprecated public static final String GET_TASKS = "android.permission.GET_TASKS";
     field public static final String GLOBAL_SEARCH = "android.permission.GLOBAL_SEARCH";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HAND_TRACKING = "android.permission.HAND_TRACKING";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HEAD_TRACKING = "android.permission.HEAD_TRACKING";
     field public static final String HIDE_OVERLAY_WINDOWS = "android.permission.HIDE_OVERLAY_WINDOWS";
     field public static final String HIGH_SAMPLING_RATE_SENSORS = "android.permission.HIGH_SAMPLING_RATE_SENSORS";
     field public static final String INSTALL_LOCATION_PROVIDER = "android.permission.INSTALL_LOCATION_PROVIDER";
@@ -295,6 +300,8 @@
     field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
     field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
     field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_COARSE = "android.permission.SCENE_UNDERSTANDING_COARSE";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_FINE = "android.permission.SCENE_UNDERSTANDING_FINE";
     field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
     field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE";
     field public static final String SEND_SMS = "android.permission.SEND_SMS";
@@ -362,6 +369,8 @@
     field public static final String SENSORS = "android.permission-group.SENSORS";
     field public static final String SMS = "android.permission-group.SMS";
     field public static final String STORAGE = "android.permission-group.STORAGE";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING = "android.permission-group.XR_TRACKING";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_SENSITIVE = "android.permission-group.XR_TRACKING_SENSITIVE";
   }
 
   public final class R {
@@ -5479,37 +5488,37 @@
     method public android.net.Uri getConditionId();
     method @Nullable public android.content.ComponentName getConfigurationActivity();
     method public long getCreationTime();
-    method @FlaggedApi("android.app.modes_api") @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects();
-    method @FlaggedApi("android.app.modes_api") @DrawableRes public int getIconResId();
+    method @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects();
+    method @DrawableRes public int getIconResId();
     method public int getInterruptionFilter();
     method public String getName();
     method public android.content.ComponentName getOwner();
-    method @FlaggedApi("android.app.modes_api") @Nullable public String getTriggerDescription();
-    method @FlaggedApi("android.app.modes_api") public int getType();
+    method @Nullable public String getTriggerDescription();
+    method public int getType();
     method @Nullable public android.service.notification.ZenPolicy getZenPolicy();
     method public boolean isEnabled();
-    method @FlaggedApi("android.app.modes_api") public boolean isManualInvocationAllowed();
+    method public boolean isManualInvocationAllowed();
     method public void setConditionId(android.net.Uri);
     method public void setConfigurationActivity(@Nullable android.content.ComponentName);
-    method @FlaggedApi("android.app.modes_api") public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects);
+    method public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects);
     method public void setEnabled(boolean);
     method public void setInterruptionFilter(int);
     method public void setName(String);
     method public void setZenPolicy(@Nullable android.service.notification.ZenPolicy);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.AutomaticZenRule> CREATOR;
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_BEDTIME = 3; // 0x3
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_DRIVING = 4; // 0x4
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_IMMERSIVE = 5; // 0x5
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_MANAGED = 7; // 0x7
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_OTHER = 0; // 0x0
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_TIME = 1; // 0x1
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_THEATER = 6; // 0x6
-    field @FlaggedApi("android.app.modes_api") public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int TYPE_BEDTIME = 3; // 0x3
+    field public static final int TYPE_DRIVING = 4; // 0x4
+    field public static final int TYPE_IMMERSIVE = 5; // 0x5
+    field public static final int TYPE_MANAGED = 7; // 0x7
+    field public static final int TYPE_OTHER = 0; // 0x0
+    field public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2
+    field public static final int TYPE_SCHEDULE_TIME = 1; // 0x1
+    field public static final int TYPE_THEATER = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
   }
 
-  @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
+  public static final class AutomaticZenRule.Builder {
     ctor public AutomaticZenRule.Builder(@NonNull android.app.AutomaticZenRule);
     ctor public AutomaticZenRule.Builder(@NonNull String, @NonNull android.net.Uri);
     method @NonNull public android.app.AutomaticZenRule build();
@@ -7127,7 +7136,7 @@
 
   public class NotificationManager {
     method public String addAutomaticZenRule(android.app.AutomaticZenRule);
-    method @FlaggedApi("android.app.modes_api") public boolean areAutomaticZenRulesUserManaged();
+    method public boolean areAutomaticZenRulesUserManaged();
     method @Deprecated public boolean areBubblesAllowed();
     method public boolean areBubblesEnabled();
     method public boolean areNotificationsEnabled();
@@ -7147,7 +7156,7 @@
     method public void deleteNotificationChannelGroup(String);
     method public android.service.notification.StatusBarNotification[] getActiveNotifications();
     method public android.app.AutomaticZenRule getAutomaticZenRule(String);
-    method @FlaggedApi("android.app.modes_api") public int getAutomaticZenRuleState(@NonNull String);
+    method public int getAutomaticZenRuleState(@NonNull String);
     method public java.util.Map<java.lang.String,android.app.AutomaticZenRule> getAutomaticZenRules();
     method public int getBubblePreference();
     method @NonNull public android.app.NotificationManager.Policy getConsolidatedNotificationPolicy();
@@ -7176,14 +7185,14 @@
     field public static final String ACTION_APP_BLOCK_STATE_CHANGED = "android.app.action.APP_BLOCK_STATE_CHANGED";
     field public static final String ACTION_AUTOMATIC_ZEN_RULE = "android.app.action.AUTOMATIC_ZEN_RULE";
     field public static final String ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED = "android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED";
-    field @FlaggedApi("android.app.modes_api") public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
+    field public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
     field public static final String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED";
     field public static final String ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED";
     field public static final String ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED";
     field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
     field public static final String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED";
-    field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4
-    field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5
+    field public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4
+    field public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5
     field public static final int AUTOMATIC_RULE_STATUS_DISABLED = 2; // 0x2
     field public static final int AUTOMATIC_RULE_STATUS_ENABLED = 1; // 0x1
     field public static final int AUTOMATIC_RULE_STATUS_REMOVED = 3; // 0x3
@@ -7197,7 +7206,7 @@
     field public static final String EXTRA_BLOCKED_STATE = "android.app.extra.BLOCKED_STATE";
     field public static final String EXTRA_NOTIFICATION_CHANNEL_GROUP_ID = "android.app.extra.NOTIFICATION_CHANNEL_GROUP_ID";
     field public static final String EXTRA_NOTIFICATION_CHANNEL_ID = "android.app.extra.NOTIFICATION_CHANNEL_ID";
-    field @FlaggedApi("android.app.modes_api") public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY";
+    field public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY";
     field public static final int IMPORTANCE_DEFAULT = 3; // 0x3
     field public static final int IMPORTANCE_HIGH = 4; // 0x4
     field public static final int IMPORTANCE_LOW = 2; // 0x2
@@ -17612,6 +17621,7 @@
     method public void setIntUniform(@NonNull String, int, int, int);
     method public void setIntUniform(@NonNull String, int, int, int, int);
     method public void setIntUniform(@NonNull String, @NonNull int[]);
+    method @FlaggedApi("com.android.graphics.hwui.flags.shader_color_space") public void setWorkingColorSpace(@Nullable android.graphics.ColorSpace);
   }
 
   @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode {
@@ -38271,7 +38281,7 @@
     field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS";
     field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS";
     field public static final String ACTION_APP_USAGE_SETTINGS = "android.settings.action.APP_USAGE_SETTINGS";
-    field @FlaggedApi("android.app.modes_api") public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
+    field public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
     field public static final String ACTION_AUTO_ROTATE_SETTINGS = "android.settings.AUTO_ROTATE_SETTINGS";
     field public static final String ACTION_BATTERY_SAVER_SETTINGS = "android.settings.BATTERY_SAVER_SETTINGS";
     field public static final String ACTION_BIOMETRIC_ENROLL = "android.settings.BIOMETRIC_ENROLL";
@@ -38365,7 +38375,7 @@
     field public static final String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled";
     field public static final String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE";
     field public static final String EXTRA_AUTHORITIES = "authorities";
-    field @FlaggedApi("android.app.modes_api") public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
+    field public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
     field public static final String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled";
     field public static final String EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED = "android.provider.extra.BIOMETRIC_AUTHENTICATORS_ALLOWED";
     field public static final String EXTRA_CHANNEL_FILTER_LIST = "android.provider.extra.CHANNEL_FILTER_LIST";
@@ -42157,9 +42167,9 @@
 
   public final class Condition implements android.os.Parcelable {
     ctor public Condition(android.net.Uri, String, int);
-    ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, int, int);
+    ctor public Condition(@Nullable android.net.Uri, @Nullable String, int, int);
     ctor public Condition(android.net.Uri, String, String, String, int, int, int);
-    ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int);
+    ctor public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int);
     ctor public Condition(android.os.Parcel);
     method public android.service.notification.Condition copy();
     method public int describeContents();
@@ -42172,10 +42182,10 @@
     field public static final int FLAG_RELEVANT_ALWAYS = 2; // 0x2
     field public static final int FLAG_RELEVANT_NOW = 1; // 0x1
     field public static final String SCHEME = "condition";
-    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_CONTEXT = 3; // 0x3
-    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_SCHEDULE = 2; // 0x2
-    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_UNKNOWN = 0; // 0x0
-    field @FlaggedApi("android.app.modes_api") public static final int SOURCE_USER_ACTION = 1; // 0x1
+    field public static final int SOURCE_CONTEXT = 3; // 0x3
+    field public static final int SOURCE_SCHEDULE = 2; // 0x2
+    field public static final int SOURCE_UNKNOWN = 0; // 0x0
+    field public static final int SOURCE_USER_ACTION = 1; // 0x1
     field public static final int STATE_ERROR = 3; // 0x3
     field public static final int STATE_FALSE = 0; // 0x0
     field public static final int STATE_TRUE = 1; // 0x1
@@ -42185,7 +42195,7 @@
     field public final android.net.Uri id;
     field public final String line1;
     field public final String line2;
-    field @FlaggedApi("android.app.modes_api") public final int source;
+    field public final int source;
     field public final int state;
     field public final String summary;
   }
@@ -42356,7 +42366,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR;
   }
 
-  @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+  public final class ZenDeviceEffects implements android.os.Parcelable {
     method public int describeContents();
     method public boolean shouldDimWallpaper();
     method public boolean shouldDisplayGrayscale();
@@ -42366,7 +42376,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.ZenDeviceEffects> CREATOR;
   }
 
-  @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+  public static final class ZenDeviceEffects.Builder {
     ctor public ZenDeviceEffects.Builder();
     ctor public ZenDeviceEffects.Builder(@NonNull android.service.notification.ZenDeviceEffects);
     method @NonNull public android.service.notification.ZenDeviceEffects build();
@@ -42388,7 +42398,7 @@
     method public int getPriorityCategoryReminders();
     method public int getPriorityCategoryRepeatCallers();
     method public int getPriorityCategorySystem();
-    method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed();
+    method public int getPriorityChannelsAllowed();
     method public int getPriorityConversationSenders();
     method public int getPriorityMessageSenders();
     method public int getVisualEffectAmbient();
@@ -42423,7 +42433,7 @@
     method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int);
-    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
+    method @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean);
@@ -45161,6 +45171,7 @@
     field public static final String KEY_RTT_UPGRADE_SUPPORTED_BOOL = "rtt_upgrade_supported_bool";
     field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = "satellite_connected_notification_throttle_millis_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int";
     field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int";
     field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DISPLAY_NAME_STRING = "satellite_display_name_string";
@@ -48733,6 +48744,7 @@
   @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager {
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void registerStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateChangeListener);
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void unregisterStateChangeListener(@NonNull android.telephony.satellite.SatelliteStateChangeListener);
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED";
   }
 
   @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public interface SatelliteStateChangeListener {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3b26e6b..0d5ec19 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -151,6 +151,8 @@
     field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES";
     field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED";
     field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_CALIBRATION = "android.permission.EYE_CALIBRATION";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING_CALIBRATION = "android.permission.FACE_TRACKING_CALIBRATION";
     field public static final String FORCE_BACK = "android.permission.FORCE_BACK";
     field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
     field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA";
@@ -168,6 +170,7 @@
     field public static final String HARDWARE_TEST = "android.permission.HARDWARE_TEST";
     field public static final String HDMI_CEC = "android.permission.HDMI_CEC";
     field @Deprecated public static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS = "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String IMPORT_XR_ANCHOR = "android.permission.IMPORT_XR_ANCHOR";
     field public static final String INJECT_EVENTS = "android.permission.INJECT_EVENTS";
     field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final String INSTALL_DEPENDENCY_SHARED_LIBRARIES = "android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES";
     field public static final String INSTALL_DPC_PACKAGES = "android.permission.INSTALL_DPC_PACKAGES";
@@ -450,6 +453,7 @@
     field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG";
     field public static final String WRITE_SMS = "android.permission.WRITE_SMS";
     field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS";
+    field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_IN_BACKGROUND = "android.permission.XR_TRACKING_IN_BACKGROUND";
   }
 
   public static final class Manifest.permission_group {
@@ -2934,6 +2938,14 @@
 
 }
 
+package android.app.supervision {
+
+  @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
+    method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled();
+  }
+
+}
+
 package android.app.time {
 
   public final class Capabilities {
@@ -3797,6 +3809,7 @@
     field public static final String SHARED_CONNECTIVITY_SERVICE = "shared_connectivity";
     field public static final String SMARTSPACE_SERVICE = "smartspace";
     field public static final String STATS_MANAGER = "stats";
+    field @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public static final String SUPERVISION_SERVICE = "supervision";
     field public static final String SYSTEM_CONFIG_SERVICE = "system_config";
     field public static final String SYSTEM_UPDATE_SERVICE = "system_update";
     field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network";
@@ -18580,6 +18593,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatelliteDataOptimizedApps();
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons();
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9e9e3c2..975c2c2 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -393,25 +393,25 @@
   }
 
   public class NotificationManager {
-    method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
+    method @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
     method @FlaggedApi("android.service.notification.notification_classification") public void allowAssistantAdjustment(@NonNull String);
     method public void cleanUpCallersAfter(long);
     method @FlaggedApi("android.service.notification.notification_classification") public void disallowAssistantAdjustment(@NonNull String);
-    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
+    method @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
     method public android.content.ComponentName getEffectsSuppressor();
     method @FlaggedApi("android.service.notification.notification_classification") @NonNull public java.util.Set<java.lang.String> getUnsupportedAdjustmentTypes();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
-    method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
+    method public boolean removeAutomaticZenRule(@NonNull String, boolean);
     method @FlaggedApi("android.service.notification.notification_classification") public void setAssistantAdjustmentKeyTypeState(int, boolean);
     method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
-    method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
+    method public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
     method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel);
   }
 
   public static class NotificationManager.Policy implements android.os.Parcelable {
-    method @FlaggedApi("android.app.modes_api") public boolean allowPriorityChannels();
+    method public boolean allowPriorityChannels();
   }
 
   public final class PendingIntent implements android.os.Parcelable {
@@ -478,8 +478,8 @@
     method public void destroy();
     method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions();
     method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle);
-    method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean);
-    method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent);
+    method @Deprecated public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean);
+    method @Deprecated public void injectInputEventToInputFilter(@NonNull android.view.InputEvent);
     method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo);
     method public void removeOverridePermissionState(int, @NonNull String);
     method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle);
@@ -490,15 +490,15 @@
   }
 
   public class UiModeManager {
-    method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay();
+    method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay();
     method public boolean isNightModeLocked();
     method public boolean isUiModeLocked();
     method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int);
     method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int);
-    field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea
-    field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9
-    field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8
-    field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff
+    field public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea
+    field public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9
+    field public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8
+    field public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff
     field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff
     field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1
     field public static final int PROJECTION_TYPE_NONE = 0; // 0x0
@@ -850,6 +850,14 @@
 
 }
 
+package android.app.supervision {
+
+  @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
+    method public void setSupervisionEnabled(boolean);
+  }
+
+}
+
 package android.app.usage {
 
   public class StorageStatsManager {
@@ -1716,7 +1724,7 @@
   }
 
   public final class ColorDisplayManager {
-    method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated();
+    method @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated();
   }
 
   public final class DisplayManager {
@@ -3237,16 +3245,16 @@
     method @Deprecated public boolean isBound();
   }
 
-  @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+  public final class ZenDeviceEffects implements android.os.Parcelable {
     method @NonNull public java.util.Set<java.lang.String> getExtraEffects();
   }
 
-  @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+  public static final class ZenDeviceEffects.Builder {
     method @NonNull public android.service.notification.ZenDeviceEffects.Builder setExtraEffects(@NonNull java.util.Set<java.lang.String>);
   }
 
   public final class ZenPolicy implements android.os.Parcelable {
-    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
+    method @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
   }
 
   public static final class ZenPolicy.Builder {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 252d23f..ee9c64f 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -292,13 +292,15 @@
  *         to the user, it must be completely restarted and restored to its previous state.</li>
  * </ul>
  *
- * <p>The following diagram shows the important state paths of an Activity.
+ * <p>The following diagram shows the important state paths of an activity.
  * The square rectangles represent callback methods you can implement to
- * perform operations when the Activity moves between states.  The colored
- * ovals are major states the Activity can be in.</p>
+ * perform operations when the activity moves between states.  The colored
+ * ovals are major states the activity can be in.</p>
  *
- * <p><img src="../../../images/activity_lifecycle.png"
- *      alt="State diagram for an Android Activity Lifecycle." border="0" /></p>
+ * <p><img class="invert"
+ *         style="display: block; margin: auto;"
+ *         src="../../../images/activity_lifecycle.png"
+ *         alt="State diagram for the Android activity lifecycle." /></p>
  *
  * <p>There are three key loops you may be interested in monitoring within your
  * activity:
@@ -505,7 +507,7 @@
  * changes.</p>
  *
  * <p>Unless you specify otherwise, a configuration change (such as a change
- * in screen orientation, language, input devices, etc) will cause your
+ * in screen orientation, language, input devices, etc.) will cause your
  * current activity to be <em>destroyed</em>, going through the normal activity
  * lifecycle process of {@link #onPause},
  * {@link #onStop}, and {@link #onDestroy} as appropriate.  If the activity
@@ -1838,7 +1840,7 @@
      *
      * <p>You can call {@link #finish} from within this function, in
      * which case onDestroy() will be immediately called after {@link #onCreate} without any of the
-     * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc)
+     * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc.)
      * executing.
      *
      * <p><em>Derived classes must call through to the super class's
@@ -2132,7 +2134,7 @@
      *
      * <p>You can call {@link #finish} from within this function, in
      * which case {@link #onStop} will be immediately called after {@link #onStart} without the
-     * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc) executing.
+     * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc.) executing.
      *
      * <p><em>Derived classes must call through to the super class's
      * implementation of this method.  If they do not, an exception will be
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index f63170a..588ca1d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -387,7 +387,7 @@
     @UnsupportedAppUsage
     private ContextImpl mSystemContext;
     @GuardedBy("this")
-    private ArrayList<WeakReference<ContextImpl>> mDisplaySystemUiContexts;
+    private ArrayList<WeakReference<Context>> mDisplaySystemUiContexts;
 
     @UnsupportedAppUsage
     static volatile IPackageManager sPackageManager;
@@ -3204,7 +3204,7 @@
     }
 
     @NonNull
-    public ContextImpl getSystemUiContext() {
+    public Context getSystemUiContext() {
         return getSystemUiContext(DEFAULT_DISPLAY);
     }
 
@@ -3214,7 +3214,7 @@
      * @see ContextImpl#createSystemUiContext(ContextImpl, int)
      */
     @NonNull
-    public ContextImpl getSystemUiContext(int displayId) {
+    public Context getSystemUiContext(int displayId) {
         synchronized (this) {
             if (mDisplaySystemUiContexts == null) {
                 mDisplaySystemUiContexts = new ArrayList<>();
@@ -3222,7 +3222,7 @@
 
             mDisplaySystemUiContexts.removeIf(contextRef -> contextRef.refersTo(null));
 
-            ContextImpl context = getSystemUiContextNoCreateLocked(displayId);
+            Context context = getSystemUiContextNoCreateLocked(displayId);
             if (context != null) {
                 return context;
             }
@@ -3233,9 +3233,20 @@
         }
     }
 
+    /**
+     * Creates a {@code SystemUiContext} for testing.
+     * <p>
+     * DO NOT use it in production code.
+     */
+    @VisibleForTesting
+    @NonNull
+    public Context createSystemUiContextForTesting(int displayId) {
+        return ContextImpl.createSystemUiContext(getSystemContext(), displayId);
+    }
+
     @Nullable
     @Override
-    public ContextImpl getSystemUiContextNoCreate() {
+    public Context getSystemUiContextNoCreate() {
         synchronized (this) {
             if (mDisplaySystemUiContexts == null) {
                 return null;
@@ -3246,9 +3257,9 @@
 
     @GuardedBy("this")
     @Nullable
-    private ContextImpl getSystemUiContextNoCreateLocked(int displayId) {
+    private Context getSystemUiContextNoCreateLocked(int displayId) {
         for (int i = 0; i < mDisplaySystemUiContexts.size(); i++) {
-            ContextImpl context = mDisplaySystemUiContexts.get(i).get();
+            Context context = mDisplaySystemUiContexts.get(i).get();
             if (context != null && context.getDisplayId() == displayId) {
                 return context;
             }
@@ -3267,7 +3278,8 @@
     public void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
         synchronized (this) {
             getSystemContext().installSystemApplicationInfo(info, classLoader);
-            getSystemUiContext().installSystemApplicationInfo(info, classLoader);
+            final ContextImpl sysUiContextImpl = ContextImpl.getImpl(getSystemUiContext());
+            sysUiContextImpl.installSystemApplicationInfo(info, classLoader);
 
             // give ourselves a default profiler
             mProfiler = new Profiler();
diff --git a/core/java/android/app/ActivityThreadInternal.java b/core/java/android/app/ActivityThreadInternal.java
index 72506b9..70876da 100644
--- a/core/java/android/app/ActivityThreadInternal.java
+++ b/core/java/android/app/ActivityThreadInternal.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.content.ComponentCallbacks2;
+import android.content.Context;
 
 import java.util.ArrayList;
 
@@ -28,7 +29,7 @@
 interface ActivityThreadInternal {
     ContextImpl getSystemContext();
 
-    ContextImpl getSystemUiContextNoCreate();
+    Context getSystemUiContextNoCreate();
 
     boolean isInDensityCompatMode();
 
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 3214bd8..2e8031d 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -840,7 +840,9 @@
      * @hide
      */
     // LINT.IfChange(write_proto)
-    public void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+    public void writeToProto(ProtoOutputStream proto, long fieldId,
+            ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream,
+            TypedXmlSerializer typedXmlSerializer) throws IOException {
         final long token = proto.start(fieldId);
         proto.write(ApplicationStartInfoProto.PID, mPid);
         proto.write(ApplicationStartInfoProto.REAL_UID, mRealUid);
@@ -850,38 +852,38 @@
         proto.write(ApplicationStartInfoProto.STARTUP_STATE, mStartupState);
         proto.write(ApplicationStartInfoProto.REASON, mReason);
         if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
-            ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream();
-            ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes);
-            TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut);
-            serializer.startDocument(null, true);
-            serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+            byteArrayOutputStream = new ByteArrayOutputStream();
+            objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+            typedXmlSerializer = Xml.resolveSerializer(objectOutputStream);
+            typedXmlSerializer.startDocument(null, true);
+            typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
             for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
-                serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
-                serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY,
+                typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
+                typedXmlSerializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY,
                         mStartupTimestampsNs.keyAt(i));
-                serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS,
+                typedXmlSerializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS,
                         mStartupTimestampsNs.valueAt(i));
-                serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
+                typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
             }
-            serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
-            serializer.endDocument();
+            typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+            typedXmlSerializer.endDocument();
             proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS,
-                    timestampsBytes.toByteArray());
-            timestampsOut.close();
+                    byteArrayOutputStream.toByteArray());
+            objectOutputStream.close();
         }
         proto.write(ApplicationStartInfoProto.START_TYPE, mStartType);
         if (mStartIntent != null) {
-            ByteArrayOutputStream intentBytes = new ByteArrayOutputStream();
-            ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes);
-            TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut);
-            serializer.startDocument(null, true);
-            serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
-            mStartIntent.saveToXml(serializer);
-            serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
-            serializer.endDocument();
+            byteArrayOutputStream = new ByteArrayOutputStream();
+            objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+            typedXmlSerializer = Xml.resolveSerializer(objectOutputStream);
+            typedXmlSerializer.startDocument(null, true);
+            typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+            mStartIntent.saveToXml(typedXmlSerializer);
+            typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+            typedXmlSerializer.endDocument();
             proto.write(ApplicationStartInfoProto.START_INTENT,
-                    intentBytes.toByteArray());
-            intentOut.close();
+                    byteArrayOutputStream.toByteArray());
+            objectOutputStream.close();
         }
         proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
         proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped);
@@ -900,7 +902,9 @@
      * @hide
      */
     // LINT.IfChange(read_proto)
-    public void readFromProto(ProtoInputStream proto, long fieldId)
+    public void readFromProto(ProtoInputStream proto, long fieldId,
+            ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream,
+            TypedXmlPullParser typedXmlPullParser)
             throws IOException, WireTypeMismatchException, ClassNotFoundException {
         final long token = proto.start(fieldId);
         while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -927,19 +931,21 @@
                     mReason = proto.readInt(ApplicationStartInfoProto.REASON);
                     break;
                 case (int) ApplicationStartInfoProto.STARTUP_TIMESTAMPS:
-                    ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes(
+                    byteArrayInputStream = new ByteArrayInputStream(proto.readBytes(
                             ApplicationStartInfoProto.STARTUP_TIMESTAMPS));
-                    ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes);
+                    objectInputStream = new ObjectInputStream(byteArrayInputStream);
                     mStartupTimestampsNs = new ArrayMap<Integer, Long>();
                     try {
-                        TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn);
-                        XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
-                        int depth = parser.getDepth();
-                        while (XmlUtils.nextElementWithin(parser, depth)) {
-                            if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) {
-                                int key = parser.getAttributeInt(null,
+                        typedXmlPullParser = Xml.resolvePullParser(objectInputStream);
+                        XmlUtils.beginDocument(typedXmlPullParser,
+                                PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+                        int depth = typedXmlPullParser.getDepth();
+                        while (XmlUtils.nextElementWithin(typedXmlPullParser, depth)) {
+                            if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(
+                                    typedXmlPullParser.getName())) {
+                                int key = typedXmlPullParser.getAttributeInt(null,
                                         PROTO_SERIALIZER_ATTRIBUTE_KEY);
-                                long ts = parser.getAttributeLong(null,
+                                long ts = typedXmlPullParser.getAttributeLong(null,
                                         PROTO_SERIALIZER_ATTRIBUTE_TS);
                                 mStartupTimestampsNs.put(key, ts);
                             }
@@ -947,23 +953,24 @@
                     } catch (XmlPullParserException e) {
                         // Timestamps lost
                     }
-                    timestampsIn.close();
+                    objectInputStream.close();
                     break;
                 case (int) ApplicationStartInfoProto.START_TYPE:
                     mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE);
                     break;
                 case (int) ApplicationStartInfoProto.START_INTENT:
-                    ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes(
+                    byteArrayInputStream = new ByteArrayInputStream(proto.readBytes(
                             ApplicationStartInfoProto.START_INTENT));
-                    ObjectInputStream intentIn = new ObjectInputStream(intentBytes);
+                    objectInputStream = new ObjectInputStream(byteArrayInputStream);
                     try {
-                        TypedXmlPullParser parser = Xml.resolvePullParser(intentIn);
-                        XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
-                        mStartIntent = Intent.restoreFromXml(parser);
+                        typedXmlPullParser = Xml.resolvePullParser(objectInputStream);
+                        XmlUtils.beginDocument(typedXmlPullParser,
+                                PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+                        mStartIntent = Intent.restoreFromXml(typedXmlPullParser);
                     } catch (XmlPullParserException e) {
                         // Intent lost
                     }
-                    intentIn.close();
+                    objectInputStream.close();
                     break;
                 case (int) ApplicationStartInfoProto.LAUNCH_MODE:
                     mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE);
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index 9d1d9c7..fa977c9 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -19,7 +19,6 @@
 import static com.android.internal.util.Preconditions.checkArgument;
 
 import android.annotation.DrawableRes;
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -52,48 +51,40 @@
      * and the value returned if the true type was added in an API level higher than the calling
      * app's targetSdk.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_UNKNOWN = -1;
     /**
      * Rule is of a known type, but not one of the specific types.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_OTHER = 0;
     /**
      * The type for rules triggered according to a time-based schedule.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_SCHEDULE_TIME = 1;
     /**
      * The type for rules triggered by calendar events.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_SCHEDULE_CALENDAR = 2;
     /**
      * The type for rules triggered by bedtime/sleeping, like time of day, or snore detection.
      *
      * <p>Only the 'Wellbeing' app may own rules of this type.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_BEDTIME = 3;
     /**
      * The type for rules triggered by driving detection, like Bluetooth connections or vehicle
      * sounds.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_DRIVING = 4;
     /**
      * The type for rules triggered by the user entering an immersive activity, like opening an app
      * using {@link WindowInsetsController#hide(int)}.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_IMMERSIVE = 5;
     /**
      * The type for rules that have a {@link ZenPolicy} that implies that the
      * device should not make sound and potentially hide some visual effects; may be triggered
      * when entering a location where silence is requested, like a theater.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_THEATER = 6;
     /**
      * The type for rules created and managed by a device owner. These rules may not be fully
@@ -101,7 +92,6 @@
      *
      * <p>Only a 'Device Owner' app may own rules of this type.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int TYPE_MANAGED = 7;
 
     /** @hide */
@@ -127,17 +117,14 @@
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_NAME = 1 << 0;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_INTERRUPTION_FILTER = 1 << 1;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_ICON = 1 << 2;
 
     private boolean enabled;
@@ -149,10 +136,8 @@
     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 = Flags.modesApi() ? TYPE_UNKNOWN : 0;
+    private int mType = TYPE_UNKNOWN;
     private int mIconResId;
     private String mTriggerDescription;
     private boolean mAllowManualInvocation;
@@ -229,8 +214,10 @@
 
     /**
      * @hide
+     * @deprecated Do not add new usages; will be removed soon.
      */
-    // TODO: b/310620812 - Remove when the flag is inlined (all system callers should use Builder).
+    // TODO: b/368247671 - Remove when modes_ui is inlined (remaining usages are in obsolete tests)
+    @Deprecated
     public AutomaticZenRule(String name, ComponentName owner, ComponentName configurationActivity,
             Uri conditionId, ZenPolicy policy, int interruptionFilter, boolean enabled,
             long creationTime) {
@@ -251,15 +238,12 @@
                 source.readParcelable(null, android.content.ComponentName.class));
         creationTime = source.readLong();
         mZenPolicy = source.readParcelable(null, ZenPolicy.class);
-        mModified = source.readInt() == ENABLED;
         mPkg = source.readString();
-        if (Flags.modesApi()) {
-            mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
-            mAllowManualInvocation = source.readBoolean();
-            mIconResId = source.readInt();
-            mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
-            mType = source.readInt();
-        }
+        mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
+        mAllowManualInvocation = source.readBoolean();
+        mIconResId = source.readInt();
+        mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+        mType = source.readInt();
     }
 
     /**
@@ -306,15 +290,6 @@
     }
 
     /**
-     * Returns whether this rule's name has been modified by the user.
-     * @hide
-     */
-    // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests.
-    public boolean isModified() {
-        return mModified;
-    }
-
-    /**
      * Gets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is
      * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}.
      */
@@ -325,7 +300,6 @@
 
     /** Gets the {@link ZenDeviceEffects} of this rule. */
     @Nullable
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public ZenDeviceEffects getDeviceEffects() {
         return mDeviceEffects;
     }
@@ -378,14 +352,6 @@
     }
 
     /**
-     * Sets modified state of this rule.
-     * @hide
-     */
-    public void setModified(boolean modified) {
-        this.mModified = modified;
-    }
-
-    /**
      * Sets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is
      * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}.
      *
@@ -404,7 +370,6 @@
      * <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule},
      * a {@code null} value here means the previous set of effects is retained.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void setDeviceEffects(@Nullable ZenDeviceEffects deviceEffects) {
         mDeviceEffects = deviceEffects;
     }
@@ -458,7 +423,6 @@
     /**
      * Gets the type of the rule.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @Type int getType() {
         return mType;
     }
@@ -467,7 +431,6 @@
      * Sets the type of the rule.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void setType(@Type int type) {
         mType = checkValidType(type);
     }
@@ -476,7 +439,6 @@
      * Gets the user visible description of when this rule is active
      * (see {@link Condition#STATE_TRUE}).
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @Nullable String getTriggerDescription() {
         return mTriggerDescription;
     }
@@ -489,7 +451,6 @@
      * "When connected to [Car Name]".
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void setTriggerDescription(@Nullable String triggerDescription) {
         mTriggerDescription = triggerDescription;
     }
@@ -497,7 +458,6 @@
     /**
      * Gets the resource id of the drawable icon for this rule.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @DrawableRes int getIconResId() {
         return mIconResId;
     }
@@ -506,7 +466,6 @@
      * Sets a resource id of a tintable vector drawable representing the rule in image form.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void setIconResId(int iconResId) {
         mIconResId = iconResId;
     }
@@ -515,7 +474,6 @@
      * Gets whether this rule can be manually activated by the user even when the triggering
      * condition for the rule is not met.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public boolean isManualInvocationAllowed() {
         return mAllowManualInvocation;
     }
@@ -525,23 +483,18 @@
      * condition for the rule is not met.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void setManualInvocationAllowed(boolean allowManualInvocation) {
         mAllowManualInvocation = allowManualInvocation;
     }
 
     /** @hide */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void validate() {
-        if (Flags.modesApi()) {
-            checkValidType(mType);
-            if (mDeviceEffects != null) {
-                mDeviceEffects.validate();
-            }
+        checkValidType(mType);
+        if (mDeviceEffects != null) {
+            mDeviceEffects.validate();
         }
     }
 
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @Type
     private static int checkValidType(@Type int type) {
         checkArgument(type >= TYPE_UNKNOWN && type <= TYPE_MANAGED,
@@ -571,39 +524,34 @@
         dest.writeParcelable(configurationActivity, 0);
         dest.writeLong(creationTime);
         dest.writeParcelable(mZenPolicy, 0);
-        dest.writeInt(mModified ? ENABLED : DISABLED);
         dest.writeString(mPkg);
-        if (Flags.modesApi()) {
-            dest.writeParcelable(mDeviceEffects, 0);
-            dest.writeBoolean(mAllowManualInvocation);
-            dest.writeInt(mIconResId);
-            dest.writeString(mTriggerDescription);
-            dest.writeInt(mType);
-        }
+        dest.writeParcelable(mDeviceEffects, 0);
+        dest.writeBoolean(mAllowManualInvocation);
+        dest.writeInt(mIconResId);
+        dest.writeString(mTriggerDescription);
+        dest.writeInt(mType);
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[')
+        return new StringBuilder(AutomaticZenRule.class.getSimpleName())
+                .append('[')
                 .append("enabled=").append(enabled)
                 .append(",name=").append(name)
+                .append(",type=").append(mType)
                 .append(",interruptionFilter=").append(interruptionFilter)
                 .append(",pkg=").append(mPkg)
                 .append(",conditionId=").append(conditionId)
                 .append(",owner=").append(owner)
                 .append(",configActivity=").append(configurationActivity)
                 .append(",creationTime=").append(creationTime)
-                .append(",mZenPolicy=").append(mZenPolicy);
-
-        if (Flags.modesApi()) {
-            sb.append(",deviceEffects=").append(mDeviceEffects)
-                    .append(",allowManualInvocation=").append(mAllowManualInvocation)
-                    .append(",iconResId=").append(mIconResId)
-                    .append(",triggerDescription=").append(mTriggerDescription)
-                    .append(",type=").append(mType);
-        }
-
-        return sb.append(']').toString();
+                .append(",mZenPolicy=").append(mZenPolicy)
+                .append(",deviceEffects=").append(mDeviceEffects)
+                .append(",allowManualInvocation=").append(mAllowManualInvocation)
+                .append(",iconResId=").append(mIconResId)
+                .append(",triggerDescription=").append(mTriggerDescription)
+                .append(']')
+                .toString();
     }
 
     /** @hide */
@@ -626,8 +574,7 @@
         if (!(o instanceof AutomaticZenRule)) return false;
         if (o == this) return true;
         final AutomaticZenRule other = (AutomaticZenRule) o;
-        boolean finalEquals = other.enabled == enabled
-                && other.mModified == mModified
+        return other.enabled == enabled
                 && Objects.equals(other.name, name)
                 && other.interruptionFilter == interruptionFilter
                 && Objects.equals(other.conditionId, conditionId)
@@ -635,27 +582,19 @@
                 && Objects.equals(other.mZenPolicy, mZenPolicy)
                 && Objects.equals(other.configurationActivity, configurationActivity)
                 && Objects.equals(other.mPkg, mPkg)
-                && other.creationTime == creationTime;
-        if (Flags.modesApi()) {
-            return finalEquals
-                    && Objects.equals(other.mDeviceEffects, mDeviceEffects)
-                    && other.mAllowManualInvocation == mAllowManualInvocation
-                    && other.mIconResId == mIconResId
-                    && Objects.equals(other.mTriggerDescription, mTriggerDescription)
-                    && other.mType == mType;
-        }
-        return finalEquals;
+                && other.creationTime == creationTime
+                && Objects.equals(other.mDeviceEffects, mDeviceEffects)
+                && other.mAllowManualInvocation == mAllowManualInvocation
+                && other.mIconResId == mIconResId
+                && Objects.equals(other.mTriggerDescription, mTriggerDescription)
+                && other.mType == mType;
     }
 
     @Override
     public int hashCode() {
-        if (Flags.modesApi()) {
-            return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
-                    configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime,
-                    mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
-        }
         return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
-                configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
+                configurationActivity, mZenPolicy, mDeviceEffects, creationTime,
+                mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<AutomaticZenRule> CREATOR
@@ -705,7 +644,6 @@
         return input;
     }
 
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final class Builder {
         private String mName;
         private ComponentName mOwner;
diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java
index 62a50db..f491e3d 100644
--- a/core/java/android/app/ConfigurationController.java
+++ b/core/java/android/app/ConfigurationController.java
@@ -169,7 +169,7 @@
 
         // Get theme outside of synchronization to avoid nested lock.
         final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme();
-        final ContextImpl systemUiContext = mActivityThread.getSystemUiContextNoCreate();
+        final Context systemUiContext = mActivityThread.getSystemUiContextNoCreate();
         final Resources.Theme systemUiTheme =
                 systemUiContext != null ? systemUiContext.getTheme() : null;
         synchronized (mResourcesManager) {
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index d8aa8b3..0519695 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -97,6 +97,7 @@
 import android.view.Display;
 import android.view.DisplayAdjustments;
 import android.view.autofill.AutofillManager.AutofillClient;
+import android.window.SystemUiContext;
 import android.window.WindowContext;
 import android.window.WindowTokenClient;
 import android.window.WindowTokenClientController;
@@ -3477,15 +3478,28 @@
      *                      {@link #createSystemContext(ActivityThread)}.
      * @param displayId The ID of the display where the UI is shown.
      */
-    static ContextImpl createSystemUiContext(ContextImpl systemContext, int displayId) {
+    static Context createSystemUiContext(ContextImpl systemContext, int displayId) {
+        // Step 1. Create a ContextImpl associated with its own resources.
         final WindowTokenClient token = new WindowTokenClient();
         final ContextImpl context = systemContext.createWindowContextBase(token, displayId);
-        token.attachContext(context);
+
+        // Step 2. Create a SystemUiContext to wrap the ContextImpl, which enables to listen to
+        // its config updates.
+        final Context systemUiContext;
+        if (com.android.window.flags.Flags.trackSystemUiContextBeforeWms()) {
+            systemUiContext = new SystemUiContext(context);
+            context.setOuterContext(systemUiContext);
+        } else {
+            systemUiContext = context;
+        }
+        token.attachContext(systemUiContext);
+
+        // Step 3. Associate the SystemUiContext with the display specified with ID.
         WindowTokenClientController.getInstance().attachToDisplayContent(token, displayId);
         context.mContextType = CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI;
         context.mOwnsToken = true;
 
-        return context;
+        return systemUiContext;
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index b9255ec..00df724 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -225,8 +225,6 @@
     ZenPolicy getDefaultZenPolicy();
     AutomaticZenRule getAutomaticZenRule(String id);
     Map<String, AutomaticZenRule> getAutomaticZenRules();
-    // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
-    List<ZenModeConfig.ZenRule> getZenRules();
     String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser);
     boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser);
     boolean removeAutomaticZenRule(String id, boolean fromUser);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0ca4a32..5dca1c7 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6002,7 +6002,7 @@
             // HUNS, which use a different layout that already accounts for that). Templates that
             // have content that will be displayed under the small icon also use a different margin.
             if (Flags.notificationsRedesignTemplates()
-                    && !p.mHeaderless && !p.mHasContentInLeftMargin) {
+                    && !p.mHeaderless && !p.mSkipTopLineAlignment) {
                 int margin = getContentMarginTop(mContext,
                         R.dimen.notification_2025_content_margin_top);
                 contentView.setViewLayoutMargin(R.id.notification_main_column,
@@ -6594,13 +6594,8 @@
             int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin);
             // Spacing between the text lines, scaling with the font size (originally in sp)
             int spacing = resources.getDimensionPixelSize(spacingRes);
-
             // Size of the text in the notification top line (originally in sp)
-            int[] textSizeAttr = new int[] { android.R.attr.textSize };
-            TypedArray typedArray = context.obtainStyledAttributes(
-                    R.style.TextAppearance_DeviceDefault_Notification_Info, textSizeAttr);
-            int textSize = typedArray.getDimensionPixelSize(0 /* index */, -1 /* default */);
-            typedArray.recycle();
+            int textSize = resources.getDimensionPixelSize(R.dimen.notification_subtext_size);
 
             // Adding up all the values as pixels
             return notifMargin + spacing + textSize;
@@ -9503,7 +9498,7 @@
                     .hideLeftIcon(isOneToOne)
                     .hideRightIcon(hideRightIcons || isOneToOne)
                     .headerTextSecondary(isHeaderless ? null : conversationTitle)
-                    .hasContentInLeftMargin(true);
+                    .skipTopLineAlignment(true);
             RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
                     isConversationLayout
                             ? mBuilder.getConversationLayoutResource()
@@ -14681,7 +14676,7 @@
         Icon mPromotedPicture;
         boolean mCallStyleActions;
         boolean mAllowTextWithProgress;
-        boolean mHasContentInLeftMargin;
+        boolean mSkipTopLineAlignment;
         int mTitleViewId;
         int mTextViewId;
         @Nullable CharSequence mTitle;
@@ -14707,7 +14702,7 @@
             mPromotedPicture = null;
             mCallStyleActions = false;
             mAllowTextWithProgress = false;
-            mHasContentInLeftMargin = false;
+            mSkipTopLineAlignment = false;
             mTitleViewId = R.id.title;
             mTextViewId = R.id.text;
             mTitle = null;
@@ -14774,8 +14769,8 @@
             return this;
         }
 
-        public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) {
-            mHasContentInLeftMargin = hasContentInLeftMargin;
+        public StandardTemplateParams skipTopLineAlignment(boolean skipTopLineAlignment) {
+            mSkipTopLineAlignment = skipTopLineAlignment;
             return this;
         }
 
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 00f896d..726999a 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -356,7 +356,6 @@
      * a DND component, the rule owner should activate any extra behavior that's part of that mode
      * in response to this broadcast.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4;
 
     /**
@@ -367,7 +366,6 @@
      * longer met) and then {@link Condition#STATE_TRUE} when the trigger criteria is freshly met,
      * or when the user re-activates it.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5;
 
     /**
@@ -415,7 +413,6 @@
      * <p>This broadcast is only sent to registered receivers and receivers in packages that have
      * been granted Notification Policy access (see {@link #isNotificationPolicyAccessGranted()}).
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED =
             "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
@@ -425,7 +422,6 @@
      * {@link #ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED} containing the new
      * {@link Policy} value.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final String EXTRA_NOTIFICATION_POLICY =
             "android.app.extra.NOTIFICATION_POLICY";
 
@@ -1726,9 +1722,8 @@
      * rule management to system settings/uis via
      * {@link Settings#ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public boolean areAutomaticZenRulesUserManaged() {
-        if (Flags.modesApi() && Flags.modesUi()) {
+        if (Flags.modesUi()) {
             PackageManager pm = mContext.getPackageManager();
             return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
                     && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
@@ -1748,21 +1743,7 @@
     public Map<String, AutomaticZenRule> getAutomaticZenRules() {
         INotificationManager service = service();
         try {
-            if (Flags.modesApi()) {
-                return service.getAutomaticZenRules();
-            } else {
-                List<ZenModeConfig.ZenRule> rules = service.getZenRules();
-                Map<String, AutomaticZenRule> ruleMap = new HashMap<>();
-                for (ZenModeConfig.ZenRule rule : rules) {
-                    AutomaticZenRule azr = new AutomaticZenRule(rule.name, rule.component,
-                            rule.configurationActivity, rule.conditionId, rule.zenPolicy,
-                            zenModeToInterruptionFilter(rule.zenMode), rule.enabled,
-                            rule.creationTime);
-                    azr.setPackageName(rule.pkg);
-                    ruleMap.put(rule.id, azr);
-                }
-                return ruleMap;
-            }
+            return service.getAutomaticZenRules();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1804,7 +1785,6 @@
 
     /** @hide */
     @TestApi
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @NonNull
     public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule,
             boolean fromUser) {
@@ -1840,7 +1820,6 @@
 
     /** @hide */
     @TestApi
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public boolean updateAutomaticZenRule(@NonNull String id,
             @NonNull AutomaticZenRule automaticZenRule, boolean fromUser) {
         INotificationManager service = service();
@@ -1860,7 +1839,6 @@
      * @param id The id of the rule
      * @return the state of the rule.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @Condition.State
     public int getAutomaticZenRuleState(@NonNull String id) {
         INotificationManager service = service();
@@ -1935,7 +1913,6 @@
 
     /** @hide */
     @TestApi
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) {
         INotificationManager service = service();
         try {
@@ -2326,7 +2303,6 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @NonNull ZenPolicy getDefaultZenPolicy() {
         INotificationManager service = service();
         try {
@@ -2693,7 +2669,7 @@
         /**
          * @hide
          */
-        public static final int STATE_CHANNELS_BYPASSING_DND = 1 << 0;
+        public static final int STATE_HAS_PRIORITY_CHANNELS = 1 << 0;
 
         /**
          * Whether the policy indicates that even priority channels are NOT permitted to bypass DND.
@@ -2918,7 +2894,7 @@
 
         @Override
         public String toString() {
-            StringBuilder sb = new StringBuilder().append("NotificationManager.Policy[")
+            return new StringBuilder().append("NotificationManager.Policy[")
                     .append("priorityCategories=")
                     .append(priorityCategoriesToString(priorityCategories))
                     .append(",priorityCallSenders=")
@@ -2928,24 +2904,19 @@
                     .append(",priorityConvSenders=")
                     .append(conversationSendersToString(priorityConversationSenders))
                     .append(",suppressedVisualEffects=")
-                    .append(suppressedEffectsToString(suppressedVisualEffects));
-            if (Flags.modesApi()) {
-                sb.append(",hasPriorityChannels=");
-            } else {
-                sb.append(",areChannelsBypassingDnd=");
-            }
-            sb.append((state == STATE_UNSET
-                    ? "unset"
-                    : ((state & STATE_CHANNELS_BYPASSING_DND) != 0)
-                            ? "true"
-                            : "false"));
-            if (Flags.modesApi()) {
-                sb.append(",allowPriorityChannels=")
-                        .append((state == STATE_UNSET
-                                ? "unset"
-                                : (allowPriorityChannels() ? "true" : "false")));
-            }
-            return sb.append("]").toString();
+                    .append(suppressedEffectsToString(suppressedVisualEffects))
+                    .append(",hasPriorityChannels=")
+                    .append((state == STATE_UNSET
+                            ? "unset"
+                            : ((state & STATE_HAS_PRIORITY_CHANNELS) != 0)
+                                    ? "true"
+                                    : "false"))
+                    .append(",allowPriorityChannels=")
+                    .append((state == STATE_UNSET
+                            ? "unset"
+                            : (allowPriorityChannels() ? "true" : "false")))
+                    .append("]")
+                    .toString();
         }
 
         /** @hide */
@@ -3220,7 +3191,6 @@
         }
 
         /** @hide **/
-        @FlaggedApi(Flags.FLAG_MODES_API)
         @TestApi // so CTS tests can read this state without having to use implementation detail
         public boolean allowPriorityChannels() {
             if (state == STATE_UNSET) {
@@ -3230,17 +3200,15 @@
         }
 
         /** @hide */
-        @FlaggedApi(Flags.FLAG_MODES_API)
         public boolean hasPriorityChannels() {
-            return (state & STATE_CHANNELS_BYPASSING_DND) != 0;
+            return (state & STATE_HAS_PRIORITY_CHANNELS) != 0;
         }
 
         /** @hide **/
-        @FlaggedApi(Flags.FLAG_MODES_API)
         public static int policyState(boolean hasPriorityChannels, boolean allowPriorityChannels) {
             int state = 0;
             if (hasPriorityChannels) {
-                state |= STATE_CHANNELS_BYPASSING_DND;
+                state |= STATE_HAS_PRIORITY_CHANNELS;
             }
             if (!allowPriorityChannels) {
                 state |= STATE_PRIORITY_CHANNELS_BLOCKED;
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 7b63ab8..3615326 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -122,7 +122,7 @@
     private static final String LOG_TAG = UiAutomation.class.getSimpleName();
 
     private static final boolean DEBUG = false;
-    private static final boolean VERBOSE = Build.IS_DEBUGGABLE;
+    private static final boolean VERBOSE = false;
 
     private static final int CONNECTION_ID_UNDEFINED = -1;
 
@@ -956,10 +956,9 @@
      * <p>
      * <strong>Note:</strong> It is caller's responsibility to recycle the event.
      * </p>
-     *
-     * @param event The event to inject.
-     * @param sync Whether to inject the event synchronously.
-     * @return Whether event injection succeeded.
+     * @param event the event to inject
+     * @param sync whether to inject the event synchronously
+     * @return {@code true} if event injection succeeded
      */
     public boolean injectInputEvent(InputEvent event, boolean sync) {
         return injectInputEvent(event, sync, true /* waitForAnimations */);
@@ -972,15 +971,21 @@
      * <strong>Note:</strong> It is caller's responsibility to recycle the event.
      * </p>
      *
-     * @param event The event to inject.
-     * @param sync  Whether to inject the event synchronously.
-     * @param waitForAnimations Whether to wait for all window container animations and surface
-     *   operations to complete.
-     * @return Whether event injection succeeded.
+     * @param event the event to inject
+     * @param sync  whether to inject the event synchronously.
+     * @param waitForAnimations whether to wait for all window container animations and surface
+     *   operations to complete
+     * @return {@code true} if event injection succeeded
      *
+     * @deprecated for CTS tests prefer inject input events using uinput
+     *   (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice).
+     *   Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used
+     *   for in-process injection.
      * @hide
      */
     @TestApi
+    @Deprecated  // Deprecated for CTS tests
+    @SuppressLint("UnflaggedApi")  // @FlaggedApi breaks previously released @TestApi, b/395889250
     public boolean injectInputEvent(@NonNull InputEvent event, boolean sync,
             boolean waitForAnimations) {
         try {
@@ -1003,9 +1008,15 @@
      * Events injected to the input subsystem using the standard {@link #injectInputEvent} method
      * skip the accessibility input filter to avoid feedback loops.
      *
+     * @deprecated for CTS tests prefer inject input events using uinput
+     *   (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice).
+     *   Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used
+     *   for in-process injection.
      * @hide
      */
     @TestApi
+    @Deprecated
+    @SuppressLint("UnflaggedApi")  // @FlaggedApi breaks previously released @TestApi, b/395889250
     public void injectInputEventToInputFilter(@NonNull InputEvent event) {
         try {
             mUiAutomationConnection.injectInputEventToInputFilter(event);
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index f6c789d5..33466dd 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -312,7 +312,6 @@
      * #getAttentionModeThemeOverlay()}: Keeps night mode as set by {@link #setNightMode(int)}.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @TestApi
     public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000;
 
@@ -321,7 +320,6 @@
      * #getAttentionModeThemeOverlay()}: Maintains night mode always on.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @TestApi
     public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001;
 
@@ -330,7 +328,6 @@
      * #getAttentionModeThemeOverlay()}: Maintains night mode always off (Light).
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @TestApi
     public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002;
 
@@ -338,7 +335,6 @@
      * Constant for {@link #getAttentionModeThemeOverlay()}: Error communication with server.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @TestApi
     public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1;
 
@@ -940,7 +936,6 @@
      *                                  {@code AttentionModeThemeOverlayType}.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
     public void setAttentionModeThemeOverlay(
             @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) {
@@ -967,7 +962,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @TestApi
     @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
     public @AttentionModeThemeOverlayReturnType int getAttentionModeThemeOverlay() {
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
index 112c5fd..8bde3a5 100644
--- a/core/java/android/app/backup/BackupRestoreEventLogger.java
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -34,9 +34,11 @@
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Class to log B&R stats for each data type that is backed up and restored by the calling app.
@@ -325,6 +327,21 @@
         }
     }
 
+    /** @hide */
+    public static String toString(DataTypeResult result) {
+        Objects.requireNonNull(result, "result cannot be null");
+        StringBuilder string = new StringBuilder("type=").append(result.mDataType)
+                .append(", successCount=").append(result.mSuccessCount)
+                .append(", failCount=").append(result.mFailCount);
+        if (!result.mErrors.isEmpty()) {
+            string.append(", errors=").append(result.mErrors);
+        }
+        if (result.mMetadataHash != null) {
+            string.append(", metadataHash=").append(Arrays.toString(result.mMetadataHash));
+        }
+        return string.toString();
+    }
+
     /**
      * Encapsulate logging results for a single data type.
      */
diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java
index b4c293e..7718d15 100644
--- a/core/java/android/app/jank/JankDataProcessor.java
+++ b/core/java/android/app/jank/JankDataProcessor.java
@@ -34,11 +34,13 @@
 /**
  * This class is responsible for associating frames received from SurfaceFlinger to active widget
  * states and logging those states back to the platform.
+ *
  * @hide
  */
 @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
 public class JankDataProcessor {
-
+    private static final String TAG = "JankDataProcessor";
+    private static final boolean DEBUG_LOGGING = false;
     private static final int MAX_IN_MEMORY_STATS = 25;
     private static final int LOG_BATCH_FREQUENCY = 50;
     private int mCurrentBatchCount = 0;
@@ -54,9 +56,10 @@
 
     /**
      * Called once per batch of JankData.
-     * @param jankData data received from SurfaceFlinger to be processed
+     *
+     * @param jankData     data received from SurfaceFlinger to be processed
      * @param activityName name of the activity that is tracking jank metrics.
-     * @param appUid the uid of the app.
+     * @param appUid       the uid of the app.
      */
     public void processJankData(List<JankData> jankData, String activityName, int appUid) {
         // add all the previous and active states to the pending states list.
@@ -211,8 +214,6 @@
      * clear any pending widget states.
      */
     public void logMetricCounts() {
-        //TODO b/374607503 when api changes are in add enum mapping for category and state.
-
         try {
             mPendingJankStats.values().forEach(stat -> {
                         FrameworkStatsLog.write(
@@ -221,15 +222,16 @@
                                 /*activity name*/ stat.getActivityName(),
                                 /*widget id*/ stat.getWidgetId(),
                                 /*refresh rate*/ stat.getRefreshRate(),
-                                /*widget category*/ 0,
-                                /*widget state*/ 0,
+                                /*widget category*/ widgetCategoryToInt(stat.getWidgetCategory()),
+                                /*widget state*/ widgetStateToInt(stat.getWidgetState()),
                                 /*total frames*/ stat.getTotalFrames(),
                                 /*janky frames*/ stat.getJankyFrames(),
-                                /*histogram*/ stat.mFrameOverrunBuckets);
+                                /*histogram*/ stat.getFrameOverrunBuckets());
                         Log.d(stat.mActivityName, stat.toString());
                         // return the pending stat to the pool it will be reset the next time its
                         // used.
                         mPendingJankStatsPool.release(stat);
+
                     }
             );
             // All stats have been recorded and added back to the pool for reuse, clear the pending
@@ -241,6 +243,96 @@
         }
     }
 
+    private int widgetCategoryToInt(String widgetCategory) {
+        switch (widgetCategory) {
+            case AppJankStats.WIDGET_CATEGORY_SCROLL -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SCROLLING;
+            }
+            case AppJankStats.WIDGET_CATEGORY_ANIMATION -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__ANIMATION;
+            }
+            case AppJankStats.WIDGET_CATEGORY_MEDIA -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__MEDIA;
+            }
+            case AppJankStats.WIDGET_CATEGORY_NAVIGATION -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__NAVIGATION;
+            }
+            case AppJankStats.WIDGET_CATEGORY_KEYBOARD -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__KEYBOARD;
+            }
+            case AppJankStats.WIDGET_CATEGORY_OTHER -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__OTHER;
+            }
+            default -> {
+                if (DEBUG_LOGGING) {
+                    Log.d(TAG, "Default Category Logged: "
+                            + AppJankStats.WIDGET_CATEGORY_UNSPECIFIED);
+                }
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__WIDGET_CATEGORY_UNSPECIFIED;
+            }
+        }
+    }
+
+    private int widgetStateToInt(String widgetState) {
+        switch (widgetState) {
+            case AppJankStats.WIDGET_STATE_NONE -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__NONE;
+            }
+            case AppJankStats.WIDGET_STATE_SCROLLING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SCROLLING;
+            }
+            case AppJankStats.WIDGET_STATE_FLINGING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__FLINGING;
+            }
+            case AppJankStats.WIDGET_STATE_SWIPING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SWIPING;
+            }
+            case AppJankStats.WIDGET_STATE_DRAGGING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__DRAGGING;
+            }
+            case AppJankStats.WIDGET_STATE_ZOOMING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__ZOOMING;
+            }
+            case AppJankStats.WIDGET_STATE_ANIMATING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__ANIMATING;
+            }
+            case AppJankStats.WIDGET_STATE_PLAYBACK -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__PLAYBACK;
+            }
+            case AppJankStats.WIDGET_STATE_TAPPING -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__TAPPING;
+            }
+            case AppJankStats.WIDGET_STATE_PREDICTIVE_BACK -> {
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__PREDICTIVE_BACK;
+            }
+            default -> {
+                if (DEBUG_LOGGING) {
+                    Log.d(TAG, "Default State Logged: "
+                            + AppJankStats.WIDGET_STATE_UNSPECIFIED);
+                }
+                return FrameworkStatsLog
+                        .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__WIDGET_STATE_UNSPECIFIED;
+            }
+        }
+    }
+
     public static final class PendingJankStat {
         private static final int NANOS_PER_MS = 1000000;
         public long processedVsyncId = -1;
@@ -268,7 +360,7 @@
 
         private int mRefreshRate;
 
-        private static final int[] sFrameOverrunHistogramBounds =  {
+        private static final int[] sFrameOverrunHistogramBounds = {
                 Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20,
                 -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25,
                 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000,
@@ -279,6 +371,7 @@
         // Histogram of frame duration overruns encoded in predetermined buckets.
         public PendingJankStat() {
         }
+
         public long getProcessedVsyncId() {
             return processedVsyncId;
         }
@@ -422,4 +515,4 @@
         }
 
     }
-}
+}
\ No newline at end of file
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
index a04f96a..9c85b09 100644
--- a/core/java/android/app/jank/JankTracker.java
+++ b/core/java/android/app/jank/JankTracker.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 import android.view.AttachedSurfaceControl;
 import android.view.Choreographer;
 import android.view.SurfaceControl;
@@ -30,16 +31,22 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * This class is responsible for registering callbacks that will receive JankData batches.
  * It handles managing the background thread that JankData will be processed on. As well as acting
  * as an intermediary between widgets and the state tracker, routing state changes to the tracker.
+ *
  * @hide
  */
 @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
 public class JankTracker {
-
+    private static final boolean DEBUG = false;
+    private static final String DEBUG_KEY = "JANKTRACKER";
+    // How long to delay the JankData listener registration.
+    //TODO b/394956095 see if this can be reduced or eliminated.
+    private static final int REGISTRATION_DELAY_MS = 1000;
     // Tracks states reported by widgets.
     private StateTracker mStateTracker;
     // Processes JankData batches and associates frames to widget states.
@@ -49,9 +56,6 @@
     private HandlerThread mHandlerThread = new HandlerThread("AppJankTracker");
     private Handler mHandler = null;
 
-    // Needed so we know when the view is attached to a window.
-    private ViewTreeObserver mViewTreeObserver;
-
     // Handle to a registered OnJankData listener.
     private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration;
 
@@ -76,6 +80,40 @@
      */
     private boolean mListenersRegistered = false;
 
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_JANK_API)
+    private final SurfaceControl.OnJankDataListener mJankDataListener =
+            new SurfaceControl.OnJankDataListener() {
+                @Override
+                public void onJankDataAvailable(
+                        @androidx.annotation.NonNull List<SurfaceControl.JankData> jankData) {
+                    if (mJankDataProcessor == null) return;
+                    mJankDataProcessor.processJankData(jankData, mActivityName, mAppUid);
+                }
+            };
+
+    private final ViewTreeObserver.OnWindowAttachListener mOnWindowAttachListener =
+            new ViewTreeObserver.OnWindowAttachListener() {
+                @Override
+                public void onWindowAttached() {
+                    getHandler().postDelayed(new Runnable() {
+                        @Override
+                        public void run() {
+                            mDecorView.getViewTreeObserver()
+                                    .removeOnWindowAttachListener(mOnWindowAttachListener);
+                            registerForJankData();
+                        }
+                    }, REGISTRATION_DELAY_MS);
+                }
+
+                // Leave this empty. Only need to know when the DecorView is attached to the Window
+                // in order to get a handle to AttachedSurfaceControl. There is no need to tie
+                // anything to when the view is detached as all un-registration code is tied to
+                // the lifecycle of the enclosing activity.
+                @Override
+                public void onWindowDetached() {
+
+                }
+            };
 
     public JankTracker(Choreographer choreographer, View decorView) {
         mStateTracker = new StateTracker(choreographer);
@@ -108,9 +146,10 @@
 
     /**
      * Will add the widget category, id and state as a UI state to associate frames to it.
+     *
      * @param widgetCategory preselected general widget category
-     * @param widgetId developer defined widget id if available.
-     * @param widgetState the current active widget state.
+     * @param widgetId       developer defined widget id if available.
+     * @param widgetState    the current active widget state.
      */
     public void addUiState(String widgetCategory, String widgetId, String widgetState) {
         if (!shouldTrack()) return;
@@ -121,9 +160,10 @@
     /**
      * Will remove the widget category, id and state as a ui state and no longer attribute frames
      * to it.
+     *
      * @param widgetCategory preselected general widget category
-     * @param widgetId developer defined widget id if available.
-     * @param widgetState no longer active widget state.
+     * @param widgetId       developer defined widget id if available.
+     * @param widgetState    no longer active widget state.
      */
     public void removeUiState(String widgetCategory, String widgetId, String widgetState) {
         if (!shouldTrack()) return;
@@ -133,10 +173,11 @@
 
     /**
      * Call to update a jank state to a different state.
+     *
      * @param widgetCategory preselected general widget category.
-     * @param widgetId developer defined widget id if available.
-     * @param currentState current state of the widget.
-     * @param nextState the state the widget will be in.
+     * @param widgetId       developer defined widget id if available.
+     * @param currentState   current state of the widget.
+     * @param nextState      the state the widget will be in.
      */
     public void updateUiState(String widgetCategory, String widgetId, String currentState,
             String nextState) {
@@ -150,10 +191,11 @@
      */
     public void enableAppJankTracking() {
         // Add the activity as a state, this will ensure we track frames to the activity without the
-        // need of a decorated widget to be used.
+        // need for a decorated widget to be used.
         // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged.
         mStateTracker.putState("NONE", mActivityName, "NONE");
         mTrackingEnabled = true;
+        registerForJankData();
     }
 
     /**
@@ -163,10 +205,12 @@
         mTrackingEnabled = false;
         // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged.
         mStateTracker.removeState("NONE", mActivityName, "NONE");
+        unregisterForJankData();
     }
 
     /**
      * Retrieve all pending widget states, this is intended for testing purposes only.
+     *
      * @param stateDataList the ArrayList that will be populated with the pending states.
      */
     @VisibleForTesting
@@ -190,16 +234,35 @@
     @VisibleForTesting
     public void forceListenerRegistration() {
         mSurfaceControl = mDecorView.getRootSurfaceControl();
-        registerForJankData();
-        // TODO b/376116199 Check if registration is good.
-        mListenersRegistered = true;
+        registerJankDataListener();
+    }
+
+    private void unregisterForJankData() {
+        if (mJankDataListenerRegistration == null) return;
+
+        if (com.android.window.flags.Flags.jankApi()) {
+            mJankDataListenerRegistration.release();
+        }
+        mJankDataListenerRegistration = null;
+        mListenersRegistered = false;
     }
 
     private void registerForJankData() {
-        if (mSurfaceControl == null) return;
-        /*
-        TODO b/376115668 Register for JankData batches from new JankTracking API
-         */
+        if (mDecorView == null) return;
+
+        mSurfaceControl = mDecorView.getRootSurfaceControl();
+
+        if (mSurfaceControl == null || mListenersRegistered) return;
+
+        // Wait a short time before registering the listener. During development it was observed
+        // that if a listener is registered too quickly after a hot or warm start no data is
+        // received b/394956095.
+        getHandler().postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                registerJankDataListener();
+            }
+        }, REGISTRATION_DELAY_MS);
     }
 
     /**
@@ -218,23 +281,30 @@
      */
     private void registerWindowListeners() {
         if (mDecorView == null) return;
-        mViewTreeObserver = mDecorView.getViewTreeObserver();
-        mViewTreeObserver.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() {
-            @Override
-            public void onWindowAttached() {
-                getHandler().postDelayed(new Runnable() {
-                    @Override
-                    public void run() {
-                        forceListenerRegistration();
-                    }
-                }, 1000);
-            }
+        mDecorView.getViewTreeObserver().addOnWindowAttachListener(mOnWindowAttachListener);
+    }
 
-            @Override
-            public void onWindowDetached() {
-                // TODO b/376116199  do we un-register the callback or just not process the data.
+    private void registerJankDataListener() {
+        if (mSurfaceControl == null) {
+            if (DEBUG) {
+                Log.d(DEBUG_KEY, "SurfaceControl is Null");
             }
-        });
+            return;
+        }
+
+        if (com.android.window.flags.Flags.jankApi()) {
+            mJankDataListenerRegistration = mSurfaceControl.registerOnJankDataListener(
+                    mHandlerThread.getThreadExecutor(), mJankDataListener);
+
+            if (mJankDataListenerRegistration
+                    == SurfaceControl.OnJankDataListenerRegistration.NONE) {
+                if (DEBUG) {
+                    Log.d(DEBUG_KEY, "OnJankDataListenerRegistration is assigned NONE");
+                }
+                return;
+            }
+            mListenersRegistered = true;
+        }
     }
 
     private Handler getHandler() {
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 9d8ab03..8e6b88c 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -26,6 +26,7 @@
   bug: "378660052"
 }
 
+# Flag for finalized API: In Nextfood but exported (and therefore must stay).
 flag {
   name: "modes_api"
   is_exported: true
@@ -82,6 +83,13 @@
 }
 
 flag {
+  name: "modes_cleanup_implicit"
+  namespace: "systemui"
+  description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI"
+  bug: "394087495"
+}
+
+flag {
   name: "api_tvextender"
   is_exported: true
   namespace: "systemui"
@@ -321,7 +329,7 @@
   name: "no_sbnholder"
   namespace: "systemui"
   description: "removes sbnholder from NLS"
-  bug: "362981561"
+  bug: "378128805"
 }
 
 flag {
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index e583302..2f67a8a 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -16,11 +16,14 @@
 
 package android.app.supervision;
 
+import android.content.Intent;
+
 /**
  * Internal IPC interface to the supervision service.
  * {@hide}
  */
 interface ISupervisionManager {
+    Intent createConfirmSupervisionCredentialsIntent();
     boolean isSupervisionEnabledForUser(int userId);
     void setSupervisionEnabledForUser(int userId, boolean enabled);
     String getActiveSupervisionAppPackage(int userId);
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index d307055..0270edf 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -16,13 +16,22 @@
 
 package android.app.supervision;
 
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_USERS;
+import static android.Manifest.permission.QUERY_USERS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.annotation.UserHandleAware;
 import android.annotation.UserIdInt;
+import android.app.supervision.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.content.Intent;
 import android.os.RemoteException;
 
 /**
@@ -31,6 +40,8 @@
  * @hide
  */
 @SystemService(Context.SUPERVISION_SERVICE)
+@SystemApi
+@FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS)
 public class SupervisionManager {
     private final Context mContext;
     @Nullable private final ISupervisionManager mService;
@@ -47,7 +58,8 @@
      *
      * @hide
      */
-    public static final String ACTION_ENABLE_SUPERVISION = "android.app.action.ENABLE_SUPERVISION";
+    public static final String ACTION_ENABLE_SUPERVISION =
+            "android.app.supervision.action.ENABLE_SUPERVISION";
 
     /**
      * Activity action: ask the human user to disable supervision for this user. Only the app that
@@ -62,7 +74,7 @@
      * @hide
      */
     public static final String ACTION_DISABLE_SUPERVISION =
-            "android.app.action.DISABLE_SUPERVISION";
+            "android.app.supervision.action.DISABLE_SUPERVISION";
 
     /** @hide */
     @UnsupportedAppUsage
@@ -72,11 +84,46 @@
     }
 
     /**
+     * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to
+     * launch the activity to verify supervision credentials.
+     *
+     * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this API
+     * is called, the launched activity still need to perform validity checks as the supervision
+     * state can change when the activity is launched. A null intent is returned if supervision is
+     * disabled at the time of this API call.
+     *
+     * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification
+     * of the supervision credentials.
+     *
+     * @hide
+     */
+    @RequiresPermission(value = android.Manifest.permission.QUERY_USERS)
+    @Nullable
+    public Intent createConfirmSupervisionCredentialsIntent() {
+        if (mService != null) {
+            try {
+                Intent result = mService.createConfirmSupervisionCredentialsIntent();
+                if (result != null) {
+                    result.prepareToEnterProcess(
+                            Intent.LOCAL_FLAG_FROM_SYSTEM, mContext.getAttributionSource());
+                }
+                return result;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns whether the device is supervised.
      *
      * @hide
      */
-    @UserHandleAware
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS)
+    @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS})
+    @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
     public boolean isSupervisionEnabled() {
         return isSupervisionEnabledForUser(mContext.getUserId());
     }
@@ -84,14 +131,10 @@
     /**
      * Returns whether the device is supervised.
      *
-     * <p>The caller must be from the same user as the target or hold the {@link
-     * android.Manifest.permission#INTERACT_ACROSS_USERS} permission.
-     *
      * @hide
      */
-    @RequiresPermission(
-            value = android.Manifest.permission.INTERACT_ACROSS_USERS,
-            conditional = true)
+    @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS})
+    @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
     public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
         if (mService != null) {
             try {
@@ -108,7 +151,8 @@
      *
      * @hide
      */
-    @UserHandleAware
+    @TestApi
+    @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
     public void setSupervisionEnabled(boolean enabled) {
         setSupervisionEnabledForUser(mContext.getUserId(), enabled);
     }
@@ -116,14 +160,9 @@
     /**
      * Sets whether the device is supervised for a given user.
      *
-     * <p>The caller must be from the same user as the target or hold the {@link
-     * android.Manifest.permission#INTERACT_ACROSS_USERS} permission.
-     *
      * @hide
      */
-    @RequiresPermission(
-            value = android.Manifest.permission.INTERACT_ACROSS_USERS,
-            conditional = true)
+    @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
     public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
         if (mService != null) {
             try {
diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig
index 232883c..94de038 100644
--- a/core/java/android/app/supervision/flags.aconfig
+++ b/core/java/android/app/supervision/flags.aconfig
@@ -64,3 +64,11 @@
   description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point"
   bug: "390500290"
 }
+
+flag {
+  name: "supervision_manager_apis"
+  is_exported: true
+  namespace: "supervision"
+  description: "Flag that enables system APIs in Supervision Manager"
+  bug: "382034839"
+}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index fcdb02a..ba1473c 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -120,6 +120,16 @@
 }
 
 flag {
+    name: "correct_virtual_display_power_state"
+    namespace: "virtual_devices"
+    description: "Fix the virtual display power state"
+    bug: "371125136"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "vdm_settings"
     namespace: "virtual_devices"
     description: "Show virtual devices in Settings"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3391e79..55d78f9b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -17,8 +17,8 @@
 package android.content;
 
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
-import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
 import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE;
+import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
 import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
 
 import android.annotation.AttrRes;
@@ -6858,6 +6858,8 @@
      * @see android.app.supervision.SupervisionManager
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(android.app.supervision.flags.Flags.FLAG_SUPERVISION_MANAGER_APIS)
     public static final String SUPERVISION_SERVICE = "supervision";
 
     /**
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index e6082d0..5c904c1 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -622,3 +622,10 @@
      description: "Add API to logout user"
      bug: "350045389"
 }
+
+flag {
+    name: "enable_moving_content_into_private_space"
+    namespace: "profile_experiences"
+    description: "Enable moving content into the Private Space"
+    bug: "360066001"
+}
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index 40c5324..36fa059 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -29,6 +29,8 @@
 import android.util.TypedValue;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.pkg.component.AconfigFlags;
+import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.XmlUtils;
 
 import dalvik.annotation.optimization.CriticalNative;
@@ -50,6 +52,7 @@
 @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK)
 public final class XmlBlock implements AutoCloseable {
     private static final boolean DEBUG=false;
+    public static final String ANDROID_RESOURCES = "http://schemas.android.com/apk/res/android";
 
     @UnsupportedAppUsage
     public XmlBlock(byte[] data) {
@@ -343,6 +346,23 @@
             if (ev == ERROR_BAD_DOCUMENT) {
                 throw new XmlPullParserException("Corrupt XML binary file");
             }
+            if (useLayoutReadwrite() && ev == START_TAG) {
+                AconfigFlags flags = ParsingPackageUtils.getAconfigFlags();
+                if (flags.skipCurrentElement(/* pkg= */ null, this)) {
+                    int depth = 1;
+                    while (depth > 0) {
+                        int ev2 = nativeNext(mParseState);
+                        if (ev2 == ERROR_BAD_DOCUMENT) {
+                            throw new XmlPullParserException("Corrupt XML binary file");
+                        } else if (ev2 == START_TAG) {
+                            depth++;
+                        } else if (ev2 == END_TAG) {
+                            depth--;
+                        }
+                    }
+                    return next();
+                }
+            }
             if (mDecNextDepth) {
                 mDepth--;
                 mDecNextDepth = false;
@@ -368,6 +388,18 @@
             }
             return ev;
         }
+
+        // Until ravenwood supports AconfigFlags, we just don't do layoutReadwriteFlags().
+        @android.ravenwood.annotation.RavenwoodReplace(
+                bug = 396458006, blockedBy = AconfigFlags.class)
+        private static boolean useLayoutReadwrite() {
+            return Flags.layoutReadwriteFlags();
+        }
+
+        private static boolean useLayoutReadwrite$ravenwood() {
+            return false;
+        }
+
         public void require(int type, String namespace, String name) throws XmlPullParserException,IOException {
             if (type != getEventType()
                 || (namespace != null && !namespace.equals( getNamespace () ) )
diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java
index 0d9db1f..7debab9 100644
--- a/core/java/android/hardware/display/ColorDisplayManager.java
+++ b/core/java/android/hardware/display/ColorDisplayManager.java
@@ -17,7 +17,6 @@
 package android.hardware.display;
 
 import android.Manifest;
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -401,7 +400,6 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(android.app.Flags.FLAG_MODES_API)
     @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
     public boolean isSaturationActivated() {
         return mManager.isSaturationActivated();
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index d891916..7850e37 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -612,6 +612,7 @@
             PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS,
             PRIVATE_EVENT_TYPE_HDR_SDR_RATIO_CHANGED,
             PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED,
+            PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface PrivateEventType {}
@@ -677,7 +678,7 @@
      * through the {@link DisplayListener#onDisplayChanged} callback method. New brightness
      * values can be retrieved via {@link android.view.Display#getBrightnessInfo()}.
      *
-     * @see #registerDisplayListener(DisplayListener, Handler, long)
+     * @see #registerDisplayListener(DisplayListener, Handler, long, long)
      *
      * @hide
      */
@@ -690,7 +691,7 @@
      *
      * Requires that {@link Display#isHdrSdrRatioAvailable()} is true.
      *
-     * @see #registerDisplayListener(DisplayListener, Handler, long)
+     * @see #registerDisplayListener(DisplayListener, Handler, long, long)
      *
      * @hide
      */
@@ -699,11 +700,19 @@
     /**
      * Event type to register for a display's connection changed.
      *
-     * @see #registerDisplayListener(DisplayListener, Handler, long)
+     * @see #registerDisplayListener(DisplayListener, Handler, long, long)
      * @hide
      */
     public static final long PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED = 1L << 2;
 
+    /**
+     * Event type to register for a display's committed state changes.
+     *
+     * @see #registerDisplayListener(DisplayListener, Handler, long, long)
+     * @hide
+     */
+    public static final long PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 3;
+
 
     /** @hide */
     public DisplayManager(Context context) {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 339dbf2..a7d610e 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -113,7 +113,8 @@
             EVENT_DISPLAY_CONNECTED,
             EVENT_DISPLAY_DISCONNECTED,
             EVENT_DISPLAY_REFRESH_RATE_CHANGED,
-            EVENT_DISPLAY_STATE_CHANGED
+            EVENT_DISPLAY_STATE_CHANGED,
+            EVENT_DISPLAY_COMMITTED_STATE_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DisplayEvent {}
@@ -128,6 +129,8 @@
     public static final int EVENT_DISPLAY_DISCONNECTED = 7;
     public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8;
     public static final int EVENT_DISPLAY_STATE_CHANGED = 9;
+    public static final int EVENT_DISPLAY_COMMITTED_STATE_CHANGED = 10;
+
 
     @LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = {
             INTERNAL_EVENT_FLAG_DISPLAY_ADDED,
@@ -139,6 +142,7 @@
             INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE,
             INTERNAL_EVENT_FLAG_DISPLAY_STATE,
             INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED,
+            INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InternalEventFlag {}
@@ -152,6 +156,8 @@
     public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6;
     public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7;
     public static final long INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED = 1L << 8;
+    public static final long INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 9;
+
 
     @UnsupportedAppUsage
     private static DisplayManagerGlobal sInstance;
@@ -1550,6 +1556,12 @@
                         mListener.onDisplayChanged(displayId);
                     }
                     break;
+                case EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+                    if ((mInternalEventFlagsMask
+                            & INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0) {
+                        mListener.onDisplayChanged(displayId);
+                    }
+                    break;
             }
             if (DEBUG) {
                 Trace.endSection();
@@ -1710,6 +1722,8 @@
                 return "EVENT_DISPLAY_REFRESH_RATE_CHANGED";
             case EVENT_DISPLAY_STATE_CHANGED:
                 return "EVENT_DISPLAY_STATE_CHANGED";
+            case EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+                return "EVENT_DISPLAY_COMMITTED_STATE_CHANGED";
         }
         return "UNKNOWN";
     }
@@ -1756,6 +1770,13 @@
                 & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) != 0) {
             baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
         }
+
+        if (Flags.committedStateSeparateEvent()) {
+            if ((privateEventFlags
+                    & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED) != 0) {
+                baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED;
+            }
+        }
         return baseEventMask;
     }
 
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
index 48c5887..586830c 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
@@ -224,6 +224,10 @@
         } catch (RemoteException e) {
             Log.d(TAG, "Unable to get sensor properties!");
         }
+
+        if (props == null) {
+            props = new SensorProps[]{};
+        }
         return props;
     }
 }
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 3d4b885..7c82abe0 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -386,7 +386,7 @@
      */
     public static boolean isTouchpadAccelerationEnabled(@NonNull Context context) {
         if (!isPointerAccelerationFeatureFlagEnabled()) {
-            return false;
+            return true;
         }
 
         return Settings.System.getIntForUser(context.getContentResolver(),
@@ -833,7 +833,7 @@
      */
     public static boolean isMousePointerAccelerationEnabled(@NonNull Context context) {
         if (!isPointerAccelerationFeatureFlagEnabled()) {
-            return false;
+            return true;
         }
 
         return Settings.System.getIntForUser(context.getContentResolver(),
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 6707098..1a9b42e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2119,7 +2119,6 @@
      * <p>
      * Output: Nothing.
      */
-    @FlaggedApi(android.app.Flags.FLAG_MODES_API)
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
             = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
@@ -2129,7 +2128,6 @@
      * <p>
      * This must be passed as an extra field to the {@link #ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}.
      */
-    @FlaggedApi(android.app.Flags.FLAG_MODES_API)
     public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID
             = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
 
diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java
index 9e02ecd..903f817 100644
--- a/core/java/android/security/FileIntegrityManager.java
+++ b/core/java/android/security/FileIntegrityManager.java
@@ -65,13 +65,7 @@
      * other fs-verity APIs.
      */
     public boolean isApkVeritySupported() {
-        try {
-            // Go through the service just to avoid exposing the vendor controlled system property
-            // to all apps.
-            return mService.isApkVeritySupported();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        return VerityUtils.isFsVeritySupported();
     }
 
     /**
diff --git a/core/java/android/security/IFileIntegrityService.aidl b/core/java/android/security/IFileIntegrityService.aidl
index c6def23..5a1a6a0 100644
--- a/core/java/android/security/IFileIntegrityService.aidl
+++ b/core/java/android/security/IFileIntegrityService.aidl
@@ -24,8 +24,6 @@
  * @hide
  */
 interface IFileIntegrityService {
-    boolean isApkVeritySupported();
-
     IInstalld.IFsveritySetupAuthToken createAuthToken(in ParcelFileDescriptor authFd);
 
     @EnforcePermission("SETUP_FSVERITY")
diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java
index 6e771f8..c375cfb 100644
--- a/core/java/android/service/notification/Condition.java
+++ b/core/java/android/service/notification/Condition.java
@@ -18,11 +18,9 @@
 
 import static com.android.internal.util.Preconditions.checkArgument;
 
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.Flags;
 import android.content.Context;
 import android.net.Uri;
 import android.os.Parcel;
@@ -105,20 +103,15 @@
     public @interface Source {}
 
     /** The state is changing due to an unknown reason. */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int SOURCE_UNKNOWN = 0;
     /** The state is changing due to an explicit user action. */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int SOURCE_USER_ACTION = 1;
     /** The state is changing due to an automatic schedule (alarm, set time, etc). */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int SOURCE_SCHEDULE = 2;
     /** The state is changing due to a change in context (such as detected driving or sleeping). */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int SOURCE_CONTEXT = 3;
 
     /** The source of, or reason for, the state change represented by this Condition. **/
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public final @Source int source; // default = SOURCE_UNKNOWN
 
     /**
@@ -145,7 +138,6 @@
      * @param state whether the mode should be activated or deactivated
      * @param source the source of, or reason for, the state change represented by this Condition
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public Condition(@Nullable Uri id, @Nullable String summary, @State int state,
                      @Source int source) {
         this(id, summary, "", "", -1, state, source, FLAG_RELEVANT_ALWAYS);
@@ -168,7 +160,6 @@
      * @param source the source of, or reason for, the state change represented by this Condition
      * @param flags flags on this condition
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public Condition(@Nullable Uri id, @Nullable String summary, @Nullable String line1,
                      @Nullable String line2, int icon, @State int state, @Source int source,
                      int flags) {
@@ -195,15 +186,13 @@
                 source.readString(),
                 source.readInt(),
                 source.readInt(),
-                Flags.modesApi() ? source.readInt() : SOURCE_UNKNOWN,
+                source.readInt(),
                 source.readInt());
     }
 
     /** @hide */
     public void validate() {
-        if (Flags.modesApi()) {
-            checkValidSource(source);
-        }
+        checkValidSource(source);
     }
 
     private static boolean isValidState(int state) {
@@ -211,11 +200,9 @@
     }
 
     private static int checkValidSource(@Source int source) {
-        if (Flags.modesApi()) {
-            checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT,
-                    "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, "
-                            + "SOURCE_SCHEDULE, or SOURCE_CONTEXT");
-        }
+        checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT,
+                "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, "
+                        + "SOURCE_SCHEDULE, or SOURCE_CONTEXT");
         return source;
     }
 
@@ -227,25 +214,21 @@
         dest.writeString(line2);
         dest.writeInt(icon);
         dest.writeInt(state);
-        if (Flags.modesApi()) {
-            dest.writeInt(this.source);
-        }
+        dest.writeInt(this.source);
         dest.writeInt(this.flags);
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder(Condition.class.getSimpleName()).append('[')
+        return new StringBuilder(Condition.class.getSimpleName()).append('[')
                 .append("state=").append(stateToString(state))
                 .append(",id=").append(id)
                 .append(",summary=").append(summary)
                 .append(",line1=").append(line1)
                 .append(",line2=").append(line2)
-                .append(",icon=").append(icon);
-        if (Flags.modesApi()) {
-            sb.append(",source=").append(sourceToString(source));
-        }
-        return sb.append(",flags=").append(flags)
+                .append(",icon=").append(icon)
+                .append(",source=").append(sourceToString(source))
+                .append(",flags=").append(flags)
                 .append(']').toString();
 
     }
@@ -279,7 +262,6 @@
      * Provides a human-readable string version of the Source enum.
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static @NonNull String sourceToString(@Source int source) {
         if (source == SOURCE_UNKNOWN) return "SOURCE_UNKNOWN";
         if (source == SOURCE_USER_ACTION) return "SOURCE_USER_ACTION";
@@ -301,25 +283,19 @@
         if (!(o instanceof Condition)) return false;
         if (o == this) return true;
         final Condition other = (Condition) o;
-        boolean finalEquals = Objects.equals(other.id, id)
+        return Objects.equals(other.id, id)
                 && Objects.equals(other.summary, summary)
                 && Objects.equals(other.line1, line1)
                 && Objects.equals(other.line2, line2)
                 && other.icon == icon
                 && other.state == state
-                && other.flags == flags;
-        if (Flags.modesApi()) {
-            return finalEquals && other.source == source;
-        }
-        return finalEquals;
+                && other.flags == flags
+                && other.source == source;
     }
 
     @Override
     public int hashCode() {
-        if (Flags.modesApi()) {
-            return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
-        }
-        return Objects.hash(id, summary, line1, line2, icon, state, flags);
+        return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
     }
 
     @Override
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index f11ce16..fbee06e1 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -16,7 +16,6 @@
 
 package android.service.notification;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
@@ -47,7 +46,6 @@
     public static final String PACKAGE_ANDROID = "android";
 
     /** Updates existing system-owned rules to use the new Modes fields (type, etc). */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static void maybeUpgradeRules(Context context, ZenModeConfig config) {
         for (ZenRule rule : config.automaticRules.values()) {
             if (isSystemOwnedRule(rule)) {
@@ -69,7 +67,6 @@
         return PACKAGE_ANDROID.equals(rule.pkg);
     }
 
-    @FlaggedApi(Flags.FLAG_MODES_API)
     private static void upgradeSystemProviderRule(Context context, ZenRule rule) {
         ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
         if (scheduleInfo != null) {
diff --git a/core/java/android/service/notification/ZenAdapters.java b/core/java/android/service/notification/ZenAdapters.java
index a122b71..4f53bfa 100644
--- a/core/java/android/service/notification/ZenAdapters.java
+++ b/core/java/android/service/notification/ZenAdapters.java
@@ -17,7 +17,6 @@
 package android.service.notification;
 
 import android.annotation.NonNull;
-import android.app.Flags;
 import android.app.NotificationManager.Policy;
 
 /**
@@ -50,7 +49,8 @@
                                 : ZenPolicy.PEOPLE_TYPE_NONE)
                 .allowReminders(policy.allowReminders())
                 .allowRepeatCallers(policy.allowRepeatCallers())
-                .allowSystem(policy.allowSystem());
+                .allowSystem(policy.allowSystem())
+                .allowPriorityChannels(policy.allowPriorityChannels());
 
         if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
             zenPolicyBuilder.showBadges(policy.showBadges())
@@ -62,10 +62,6 @@
                     .showStatusBarIcons(policy.showStatusBarIcons());
         }
 
-        if (Flags.modesApi()) {
-            zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
-        }
-
         return zenPolicyBuilder.build();
     }
 
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 06bd255..d88fb3e3 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -16,12 +16,10 @@
 
 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;
 
@@ -37,7 +35,6 @@
  * Represents the set of device effects (affecting display and device behavior in general) that
  * are applied whenever an {@link android.app.AutomaticZenRule} is active.
  */
-@FlaggedApi(Flags.FLAG_MODES_API)
 public final class ZenDeviceEffects implements Parcelable {
 
     /**
@@ -157,7 +154,6 @@
     }
 
     /** @hide */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public void validate() {
         int extraEffectsLength = 0;
         for (String extraEffect : mExtraEffects) {
@@ -435,7 +431,6 @@
     }
 
     /** Builder class for {@link ZenDeviceEffects} objects. */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final class Builder {
 
         private boolean mGrayscale;
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4f459aa..6f94c1b 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -228,7 +228,7 @@
     private static final boolean DEFAULT_ALLOW_CONV = true;
     private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
     private static final boolean DEFAULT_ALLOW_PRIORITY_CHANNELS = true;
-    private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false;
+    private static final boolean DEFAULT_HAS_PRIORITY_CHANNELS = false;
     // Default setting here is 010011101 = 157
     private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS =
             SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
@@ -242,9 +242,6 @@
     public static final int XML_VERSION_MODES_API = 11;
     public static final int XML_VERSION_MODES_UI = 12;
 
-    // TODO: b/310620812, b/344831624 - Update XML_VERSION and update default_zen_config.xml
-    //  accordingly when modes_api / modes_ui are inlined.
-    private static final int XML_VERSION_PRE_MODES = 10;
     public static final String ZEN_TAG = "zen";
     private static final String ZEN_ATT_VERSION = "version";
     private static final String ZEN_ATT_USER = "user";
@@ -269,7 +266,7 @@
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
     private static final String STATE_TAG = "state";
-    private static final String STATE_ATT_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
+    private static final String STATE_HAS_PRIORITY_CHANNELS = "areChannelsBypassingDnd";
 
     // zen policy visual effects attributes
     private static final String SHOW_ATT_FULL_SCREEN_INTENT = "showFullScreenIntent";
@@ -303,7 +300,6 @@
     private static final String RULE_ATT_CONDITION_ID = "conditionId";
     private static final String RULE_ATT_CREATION_TIME = "creationTime";
     private static final String RULE_ATT_ENABLER = "enabler";
-    private static final String RULE_ATT_MODIFIED = "modified";
     private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable";
     private static final String RULE_ATT_TYPE = "type";
     private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields";
@@ -313,6 +309,7 @@
     private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
     private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
     private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride";
+    private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation";
 
     private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
     private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -348,11 +345,11 @@
     public int allowConversationsFrom = DEFAULT_ALLOW_CONV_FROM;
     public int user = UserHandle.USER_SYSTEM;
     public int suppressedVisualEffects = DEFAULT_SUPPRESSED_VISUAL_EFFECTS;
-    // Note that when the modes_api flag is true, the areChannelsBypassingDnd boolean only tracks
-    // whether the current user has any priority channels. These channels may bypass DND when
-    // allowPriorityChannels is true.
-    // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined.
-    public boolean areChannelsBypassingDnd = DEFAULT_CHANNELS_BYPASSING_DND;
+    /**
+     * Whether the current user has any priority channels. These channels may bypass DND when
+     * {@link #allowPriorityChannels} is true.
+     */
+    public boolean hasPriorityChannels = DEFAULT_HAS_PRIORITY_CHANNELS;
     public boolean allowPriorityChannels = DEFAULT_ALLOW_PRIORITY_CHANNELS;
     public int version;
 
@@ -384,22 +381,18 @@
         user = source.readInt();
         manualRule = source.readParcelable(null, ZenRule.class);
         readRulesFromParcel(automaticRules, source);
-        if (Flags.modesApi()) {
-            readRulesFromParcel(deletedRules, source);
-        }
+        readRulesFromParcel(deletedRules, source);
         if (!Flags.modesUi()) {
             allowAlarms = source.readInt() == 1;
             allowMedia = source.readInt() == 1;
             allowSystem = source.readInt() == 1;
             suppressedVisualEffects = source.readInt();
         }
-        areChannelsBypassingDnd = source.readInt() == 1;
+        hasPriorityChannels = source.readInt() == 1;
         if (!Flags.modesUi()) {
             allowConversations = source.readBoolean();
             allowConversationsFrom = source.readInt();
-            if (Flags.modesApi()) {
-                allowPriorityChannels = source.readBoolean();
-            }
+            allowPriorityChannels = source.readBoolean();
         }
     }
 
@@ -493,22 +486,18 @@
         dest.writeInt(user);
         dest.writeParcelable(manualRule, 0);
         writeRulesToParcel(automaticRules, dest);
-        if (Flags.modesApi()) {
-            writeRulesToParcel(deletedRules, dest);
-        }
+        writeRulesToParcel(deletedRules, dest);
         if (!Flags.modesUi()) {
             dest.writeInt(allowAlarms ? 1 : 0);
             dest.writeInt(allowMedia ? 1 : 0);
             dest.writeInt(allowSystem ? 1 : 0);
             dest.writeInt(suppressedVisualEffects);
         }
-        dest.writeInt(areChannelsBypassingDnd ? 1 : 0);
+        dest.writeInt(hasPriorityChannels ? 1 : 0);
         if (!Flags.modesUi()) {
             dest.writeBoolean(allowConversations);
             dest.writeInt(allowConversationsFrom);
-            if (Flags.modesApi()) {
-                dest.writeBoolean(allowPriorityChannels);
-            }
+            dest.writeBoolean(allowPriorityChannels);
         }
     }
 
@@ -549,17 +538,13 @@
                             (allowConversationsFrom))
                     .append("\nsuppressedVisualEffects=").append(suppressedVisualEffects);
         }
-        if (Flags.modesApi()) {
-            sb.append("\nhasPriorityChannels=").append(areChannelsBypassingDnd);
-            sb.append(",allowPriorityChannels=").append(allowPriorityChannels);
-        } else {
-            sb.append("\nareChannelsBypassingDnd=").append(areChannelsBypassingDnd);
-        }
+
+        sb.append("\nhasPriorityChannels=").append(hasPriorityChannels);
+        sb.append(",allowPriorityChannels=").append(allowPriorityChannels);
         sb.append(",\nautomaticRules=").append(rulesToString(automaticRules));
         sb.append(",\nmanualRule=").append(manualRule);
-        if (Flags.modesApi()) {
-            sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
-        }
+        sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
+
         return sb.append(']').toString();
     }
 
@@ -854,7 +839,7 @@
         final ZenModeConfig other = (ZenModeConfig) o;
         // The policy fields that live on config are compared directly because the fields will
         // contain data until MODES_UI is rolled out/cleaned up.
-        boolean eq = other.allowAlarms == allowAlarms
+        return other.allowAlarms == allowAlarms
                 && other.allowMedia == allowMedia
                 && other.allowSystem == allowSystem
                 && other.allowCalls == allowCalls
@@ -868,35 +853,23 @@
                 && Objects.equals(other.automaticRules, automaticRules)
                 && Objects.equals(other.manualRule, manualRule)
                 && other.suppressedVisualEffects == suppressedVisualEffects
-                && other.areChannelsBypassingDnd == areChannelsBypassingDnd
+                && other.hasPriorityChannels == hasPriorityChannels
                 && other.allowConversations == allowConversations
-                && other.allowConversationsFrom == allowConversationsFrom;
-        if (Flags.modesApi()) {
-            return eq
-                    && Objects.equals(other.deletedRules, deletedRules)
-                    && other.allowPriorityChannels == allowPriorityChannels;
-        }
-        return eq;
+                && other.allowConversationsFrom == allowConversationsFrom
+                && Objects.equals(other.deletedRules, deletedRules)
+                && other.allowPriorityChannels == allowPriorityChannels;
     }
 
     @Override
     public int hashCode() {
         // The policy fields that live on config are compared directly because the fields will
         // contain data until MODES_UI is rolled out/cleaned up.
-        if (Flags.modesApi()) {
-            return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls,
-                    allowRepeatCallers, allowMessages,
-                    allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents,
-                    user, automaticRules, manualRule,
-                    suppressedVisualEffects, areChannelsBypassingDnd, allowConversations,
-                    allowConversationsFrom, allowPriorityChannels);
-        }
         return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls,
                 allowRepeatCallers, allowMessages,
                 allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents,
                 user, automaticRules, manualRule,
-                suppressedVisualEffects, areChannelsBypassingDnd, allowConversations,
-                allowConversationsFrom);
+                suppressedVisualEffects, hasPriorityChannels, allowConversations,
+                allowConversationsFrom, allowPriorityChannels);
     }
 
     private static String toDayList(int[] days) {
@@ -952,10 +925,8 @@
     public static int getCurrentXmlVersion() {
         if (Flags.modesUi()) {
             return XML_VERSION_MODES_UI;
-        } else if (Flags.modesApi()) {
-            return XML_VERSION_MODES_API;
         } else {
-            return XML_VERSION_PRE_MODES;
+            return XML_VERSION_MODES_API;
         }
     }
 
@@ -1006,10 +977,8 @@
                     rt.allowMedia = safeBoolean(parser, ALLOW_ATT_MEDIA,
                             DEFAULT_ALLOW_MEDIA);
                     rt.allowSystem = safeBoolean(parser, ALLOW_ATT_SYSTEM, DEFAULT_ALLOW_SYSTEM);
-                    if (Flags.modesApi()) {
-                        rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS,
-                                DEFAULT_ALLOW_PRIORITY_CHANNELS);
-                    }
+                    rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS,
+                            DEFAULT_ALLOW_PRIORITY_CHANNELS);
 
                     // migrate old suppressed visual effects fields, if they still exist in the xml
                     Boolean allowWhenScreenOff = unsafeBoolean(parser, ALLOW_ATT_SCREEN_OFF);
@@ -1054,13 +1023,12 @@
                     } else {
                         readRuleCount++;
                     }
-                } else if (AUTOMATIC_TAG.equals(tag)
-                        || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) {
+                } else if (AUTOMATIC_TAG.equals(tag) || AUTOMATIC_DELETED_TAG.equals(tag)) {
                     final String id = parser.getAttributeValue(null, RULE_ATT_ID);
                     if (id != null) {
                         final ZenRule automaticRule = readRuleXml(parser);
                         automaticRule.id = id;
-                        if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) {
+                        if (AUTOMATIC_DELETED_TAG.equals(tag)) {
                             String deletedRuleKey = deletedRuleKey(automaticRule);
                             if (deletedRuleKey != null) {
                                 rt.deletedRules.put(deletedRuleKey, automaticRule);
@@ -1071,8 +1039,8 @@
                         }
                     }
                 } else if (STATE_TAG.equals(tag)) {
-                    rt.areChannelsBypassingDnd = safeBoolean(parser,
-                            STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND);
+                    rt.hasPriorityChannels = safeBoolean(parser,
+                            STATE_HAS_PRIORITY_CHANNELS, DEFAULT_HAS_PRIORITY_CHANNELS);
                 }
             }
             if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
@@ -1149,9 +1117,7 @@
         out.attributeBoolean(null, ALLOW_ATT_SYSTEM, allowSystem);
         out.attributeBoolean(null, ALLOW_ATT_CONV, allowConversations);
         out.attributeInt(null, ALLOW_ATT_CONV_FROM, allowConversationsFrom);
-        if (Flags.modesApi()) {
-            out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels);
-        }
+        out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels);
         out.endTag(null, ALLOW_TAG);
 
         out.startTag(null, DISALLOW_TAG);
@@ -1174,7 +1140,7 @@
             out.endTag(null, AUTOMATIC_TAG);
             writtenRuleCount++;
         }
-        if (Flags.modesApi() && !forBackup) {
+        if (!forBackup) {
             for (int i = 0; i < deletedRules.size(); i++) {
                 final ZenRule deletedRule = deletedRules.valueAt(i);
                 out.startTag(null, AUTOMATIC_DELETED_TAG);
@@ -1185,7 +1151,7 @@
         }
 
         out.startTag(null, STATE_TAG);
-        out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd);
+        out.attributeBoolean(null, STATE_HAS_PRIORITY_CHANNELS, hasPriorityChannels);
         out.endTag(null, STATE_TAG);
 
         out.endTag(null, ZEN_TAG);
@@ -1212,39 +1178,29 @@
         rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
         rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER);
         rt.condition = readConditionXml(parser);
-
-        if (!Flags.modesApi() && rt.zenMode != ZEN_MODE_IMPORTANT_INTERRUPTIONS
-                && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) {
-            // all default rules and user created rules updated to zenMode important interruptions
-            Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name);
-            rt.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        }
-        rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false);
         rt.zenPolicy = readZenPolicyXml(parser);
-        if (Flags.modesApi()) {
-            rt.zenDeviceEffects = readZenDeviceEffectsXml(parser);
-            rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
-            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);
-            rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
-            rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
-                    DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
-            Long deletionInstant = tryParseLong(
-                    parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
-            if (deletionInstant != null) {
-                rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
-            }
-            if (Flags.modesUi()) {
-                rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
-                        ORIGIN_UNKNOWN);
-                rt.legacySuppressedEffects = safeInt(parser,
-                        RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
-                rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE,
-                        ZenRule.OVERRIDE_NONE);
+        rt.zenDeviceEffects = readZenDeviceEffectsXml(parser);
+        rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
+        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);
+        rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
+        rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
+                DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
+        rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null);
+        if (Flags.modesUi()) {
+            rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
+                    ORIGIN_UNKNOWN);
+            rt.legacySuppressedEffects = safeInt(parser,
+                    RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
+            rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE,
+                    ZenRule.OVERRIDE_NONE);
+            if (Flags.modesCleanupImplicit()) {
+                rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null);
             }
         }
+
         return rt;
     }
 
@@ -1278,35 +1234,39 @@
         if (rule.zenPolicy != null) {
             writeZenPolicyXml(rule.zenPolicy, out);
         }
-        if (Flags.modesApi() && rule.zenDeviceEffects != null) {
+        if (rule.zenDeviceEffects != null) {
             writeZenDeviceEffectsXml(rule.zenDeviceEffects, out);
         }
-        out.attributeBoolean(null, RULE_ATT_MODIFIED, rule.modified);
-        if (Flags.modesApi()) {
-            out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
-            if (rule.iconResName != null) {
-                out.attribute(null, RULE_ATT_ICON, rule.iconResName);
+        out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
+        if (rule.iconResName != null) {
+            out.attribute(null, RULE_ATT_ICON, rule.iconResName);
+        }
+        if (rule.triggerDescription != null) {
+            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);
+        out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
+        out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
+                rule.zenDeviceEffectsUserModifiedFields);
+        writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant);
+        if (Flags.modesUi()) {
+            out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+            out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
+                    rule.legacySuppressedEffects);
+            if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) {
+                out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride);
             }
-            if (rule.triggerDescription != null) {
-                out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
+            if (Flags.modesCleanupImplicit()) {
+                writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation);
             }
-            out.attributeInt(null, RULE_ATT_TYPE, rule.type);
-            out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
-            out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
-            out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
-                    rule.zenDeviceEffectsUserModifiedFields);
-            if (rule.deletionInstant != null) {
-                out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
-                        rule.deletionInstant.toEpochMilli());
-            }
-            if (Flags.modesUi()) {
-                out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
-                out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
-                        rule.legacySuppressedEffects);
-                if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) {
-                    out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride);
-                }
-            }
+        }
+    }
+
+    private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att,
+            @Nullable Instant instant) throws IOException {
+        if (instant != null) {
+            out.attributeLong(null, att, instant.toEpochMilli());
         }
     }
 
@@ -1320,12 +1280,8 @@
         final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
         final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
         try {
-            if (Flags.modesApi()) {
-                final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN);
-                return new Condition(id, summary, line1, line2, icon, state, source, flags);
-            } else {
-                return new Condition(id, summary, line1, line2, icon, state, flags);
-            }
+            final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN);
+            return new Condition(id, summary, line1, line2, icon, state, source, flags);
         } catch (IllegalArgumentException e) {
             Slog.w(TAG, "Unable to read condition xml", e);
             return null;
@@ -1339,9 +1295,7 @@
         out.attribute(null, CONDITION_ATT_LINE2, c.line2);
         out.attributeInt(null, CONDITION_ATT_ICON, c.icon);
         out.attributeInt(null, CONDITION_ATT_STATE, c.state);
-        if (Flags.modesApi()) {
-            out.attributeInt(null, CONDITION_ATT_SOURCE, c.source);
-        }
+        out.attributeInt(null, CONDITION_ATT_SOURCE, c.source);
         out.attributeInt(null, CONDITION_ATT_FLAGS, c.flags);
     }
 
@@ -1363,12 +1317,11 @@
         final int system = safeInt(parser, ALLOW_ATT_SYSTEM, ZenPolicy.STATE_UNSET);
         final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET);
         final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET);
-        if (Flags.modesApi()) {
-            final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
-            if (channels != ZenPolicy.STATE_UNSET) {
-                builder.allowPriorityChannels(channels == STATE_ALLOW);
-                policySet = true;
-            }
+        final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
+
+        if (channels != ZenPolicy.STATE_UNSET) {
+            builder.allowPriorityChannels(channels == STATE_ALLOW);
+            policySet = true;
         }
 
         if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
@@ -1478,10 +1431,7 @@
         writeZenPolicyState(SHOW_ATT_AMBIENT, policy.getVisualEffectAmbient(), out);
         writeZenPolicyState(SHOW_ATT_NOTIFICATION_LIST, policy.getVisualEffectNotificationList(),
                 out);
-
-        if (Flags.modesApi()) {
-            writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
-        }
+        writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
     }
 
     private static void writeZenPolicyState(String attr, int val, TypedXmlSerializer out)
@@ -1495,7 +1445,7 @@
             if (val != ZenPolicy.CONVERSATION_SENDERS_UNSET) {
                 out.attributeInt(null, attr, val);
             }
-        } else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
+        } else if (Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
             if (val != ZenPolicy.STATE_UNSET) {
                 out.attributeInt(null, attr, val);
             }
@@ -1506,7 +1456,6 @@
         }
     }
 
-    @FlaggedApi(Flags.FLAG_MODES_API)
     @Nullable
     private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) {
         ZenDeviceEffects deviceEffects =
@@ -1539,7 +1488,6 @@
         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,
@@ -1659,6 +1607,19 @@
         return values;
     }
 
+    @Nullable
+    private static Instant safeInstant(TypedXmlPullParser parser, String att,
+            @Nullable Instant defValue) {
+        final String strValue = parser.getAttributeValue(null, att);
+        if (!TextUtils.isEmpty(strValue)) {
+            Long longValue = tryParseLong(strValue, null);
+            if (longValue != null) {
+                return Instant.ofEpochMilli(longValue);
+            }
+        }
+        return defValue;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -1732,9 +1693,7 @@
                     (suppressedVisualEffects & Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) == 0);
         }
 
-        if (Flags.modesApi()) {
-            builder.allowPriorityChannels(allowPriorityChannels);
-        }
+        builder.allowPriorityChannels(allowPriorityChannels);
         return builder.build();
     }
 
@@ -1860,12 +1819,9 @@
             suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
         }
 
-        int state = defaultPolicy.state;
-        if (Flags.modesApi()) {
-            state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
-                    ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(),
-                            DEFAULT_ALLOW_PRIORITY_CHANNELS));
-        }
+        int state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
+                ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(),
+                        DEFAULT_ALLOW_PRIORITY_CHANNELS));
 
         return new NotificationManager.Policy(priorityCategories, callSenders,
                 messageSenders, suppressedVisualEffects, state, conversationSenders);
@@ -1930,7 +1886,7 @@
             priorityMessageSenders = peopleTypeToPrioritySenders(
                     manualRule.zenPolicy.getPriorityMessageSenders(), DEFAULT_SOURCE);
 
-            state = Policy.policyState(areChannelsBypassingDnd,
+            state = Policy.policyState(hasPriorityChannels,
                     manualRule.zenPolicy.getPriorityChannelsAllowed() != STATE_DISALLOW);
 
             boolean suppressFullScreenIntent = !manualRule.zenPolicy.isVisualEffectAllowed(
@@ -2030,10 +1986,7 @@
             priorityConversationSenders = zenPolicyConversationSendersToNotificationPolicy(
                     getAllowConversationsFrom(), priorityConversationSenders);
 
-            state = areChannelsBypassingDnd ? Policy.STATE_CHANNELS_BYPASSING_DND : 0;
-            if (Flags.modesApi()) {
-                state = Policy.policyState(areChannelsBypassingDnd, allowPriorityChannels);
-            }
+            state = Policy.policyState(hasPriorityChannels, allowPriorityChannels);
             suppressedVisualEffects = getSuppressedVisualEffects();
         }
 
@@ -2114,13 +2067,11 @@
                     policy.priorityConversationSenders,
                     allowConversationsFrom);
             if (policy.state != Policy.STATE_UNSET) {
-                if (Flags.modesApi()) {
-                    setAllowPriorityChannels(policy.allowPriorityChannels());
-                }
+                setAllowPriorityChannels(policy.allowPriorityChannels());
             }
         }
         if (policy.state != Policy.STATE_UNSET) {
-            areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+            hasPriorityChannels = (policy.state & Policy.STATE_HAS_PRIORITY_CHANNELS) != 0;
         }
     }
 
@@ -2618,8 +2569,9 @@
         @UnsupportedAppUsage
         public boolean enabled;
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        // TODO: b/368247671 - Obsolete with MODES_UI; delete when the flag is inlined
         @Deprecated
-        public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI
+        public boolean snoozing; // user manually disabled this instance.
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         public String name;              // required for automatic
         @UnsupportedAppUsage
@@ -2635,9 +2587,7 @@
         // package name, only used for manual rules when they have turned DND on.
         public String enabler;
         public ZenPolicy zenPolicy;
-        @FlaggedApi(Flags.FLAG_MODES_API)
         @Nullable public ZenDeviceEffects zenDeviceEffects;
-        public boolean modified;    // rule has been modified from initial creation
         public String pkg;
         @AutomaticZenRule.Type
         public int type = AutomaticZenRule.TYPE_UNKNOWN;
@@ -2668,6 +2618,18 @@
         @ConditionOverride
         int conditionOverride = OVERRIDE_NONE;
 
+        /**
+         * Last time at which the rule was activated (for any reason, including overrides).
+         * If {@code null}, the rule has never been activated since its creation.
+         *
+         * <p>Note that this was previously untracked, so it will also be {@code null} for rules
+         * created before we started tracking and never activated since -- make sure to account for
+         * it, for example by falling back to {@link #creationTime} in logic involving this field.
+         */
+        @Nullable
+        @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT)
+        public Instant lastActivation;
+
         public ZenRule() { }
 
         public ZenRule(Parcel source) {
@@ -2689,46 +2651,45 @@
                 enabler = source.readString();
             }
             zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
-            if (Flags.modesApi()) {
-                zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
-            }
-            modified = source.readInt() == 1;
+            zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
             pkg = source.readString();
-            if (Flags.modesApi()) {
-                allowManualInvocation = source.readBoolean();
-                iconResName = source.readString();
-                triggerDescription = source.readString();
-                type = source.readInt();
-                userModifiedFields = source.readInt();
-                zenPolicyUserModifiedFields = source.readInt();
-                zenDeviceEffectsUserModifiedFields = source.readInt();
-                if (source.readInt() == 1) {
-                    deletionInstant = Instant.ofEpochMilli(source.readLong());
-                }
-                if (Flags.modesUi()) {
-                    disabledOrigin = source.readInt();
-                    legacySuppressedEffects = source.readInt();
-                    conditionOverride = source.readInt();
+            allowManualInvocation = source.readBoolean();
+            iconResName = source.readString();
+            triggerDescription = source.readString();
+            type = source.readInt();
+            userModifiedFields = source.readInt();
+            zenPolicyUserModifiedFields = source.readInt();
+            zenDeviceEffectsUserModifiedFields = source.readInt();
+            if (source.readInt() == 1) {
+                deletionInstant = Instant.ofEpochMilli(source.readLong());
+            }
+            if (Flags.modesUi()) {
+                disabledOrigin = source.readInt();
+                legacySuppressedEffects = source.readInt();
+                conditionOverride = source.readInt();
+                if (Flags.modesCleanupImplicit()) {
+                    if (source.readInt() == 1) {
+                        lastActivation = Instant.ofEpochMilli(source.readLong());
+                    }
                 }
             }
         }
 
         /**
-         * Whether this ZenRule can be updated by an app. In general, rules that have been
-         * customized by the user cannot be further updated by an app, with some exceptions:
+         * Whether this ZenRule has been customized by the user in any way.
+
+         * <p>In general, rules that have been customized by the user cannot be further updated by
+         * an app, with some exceptions:
          * <ul>
          *     <li>Non user-configurable fields, like type, icon, configurationActivity, etc.
          *     <li>Name, if the name was not specifically modified by the user (to support language
          *          switches).
          * </ul>
          */
-        @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
-                    && zenPolicyUserModifiedFields == 0
-                    && zenDeviceEffectsUserModifiedFields == 0;
+        public boolean isUserModified() {
+            return userModifiedFields != 0
+                    || zenPolicyUserModifiedFields != 0
+                    || zenDeviceEffectsUserModifiedFields != 0;
         }
 
         @Override
@@ -2765,29 +2726,32 @@
                 dest.writeInt(0);
             }
             dest.writeParcelable(zenPolicy, 0);
-            if (Flags.modesApi()) {
-                dest.writeParcelable(zenDeviceEffects, 0);
-            }
-            dest.writeInt(modified ? 1 : 0);
+            dest.writeParcelable(zenDeviceEffects, 0);
             dest.writeString(pkg);
-            if (Flags.modesApi()) {
-                dest.writeBoolean(allowManualInvocation);
-                dest.writeString(iconResName);
-                dest.writeString(triggerDescription);
-                dest.writeInt(type);
-                dest.writeInt(userModifiedFields);
-                dest.writeInt(zenPolicyUserModifiedFields);
-                dest.writeInt(zenDeviceEffectsUserModifiedFields);
-                if (deletionInstant != null) {
-                    dest.writeInt(1);
-                    dest.writeLong(deletionInstant.toEpochMilli());
-                } else {
-                    dest.writeInt(0);
-                }
-                if (Flags.modesUi()) {
-                    dest.writeInt(disabledOrigin);
-                    dest.writeInt(legacySuppressedEffects);
-                    dest.writeInt(conditionOverride);
+            dest.writeBoolean(allowManualInvocation);
+            dest.writeString(iconResName);
+            dest.writeString(triggerDescription);
+            dest.writeInt(type);
+            dest.writeInt(userModifiedFields);
+            dest.writeInt(zenPolicyUserModifiedFields);
+            dest.writeInt(zenDeviceEffectsUserModifiedFields);
+            if (deletionInstant != null) {
+                dest.writeInt(1);
+                dest.writeLong(deletionInstant.toEpochMilli());
+            } else {
+                dest.writeInt(0);
+            }
+            if (Flags.modesUi()) {
+                dest.writeInt(disabledOrigin);
+                dest.writeInt(legacySuppressedEffects);
+                dest.writeInt(conditionOverride);
+                if (Flags.modesCleanupImplicit()) {
+                    if (lastActivation != null) {
+                        dest.writeInt(1);
+                        dest.writeLong(lastActivation.toEpochMilli());
+                    } else {
+                        dest.writeInt(0);
+                    }
                 }
             }
         }
@@ -2816,34 +2780,33 @@
                     .append(",creationTime=").append(creationTime)
                     .append(",enabler=").append(enabler)
                     .append(",zenPolicy=").append(zenPolicy)
-                    .append(",modified=").append(modified)
-                    .append(",condition=").append(condition);
-
-            if (Flags.modesApi()) {
-                sb.append(",deviceEffects=").append(zenDeviceEffects)
-                        .append(",allowManualInvocation=").append(allowManualInvocation)
-                        .append(",iconResName=").append(iconResName)
-                        .append(",triggerDescription=").append(triggerDescription)
-                        .append(",type=").append(type);
-                if (userModifiedFields != 0) {
-                    sb.append(",userModifiedFields=")
-                            .append(AutomaticZenRule.fieldsToString(userModifiedFields));
-                }
-                if (zenPolicyUserModifiedFields != 0) {
-                    sb.append(",zenPolicyUserModifiedFields=")
-                            .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields));
-                }
-                if (zenDeviceEffectsUserModifiedFields != 0) {
-                    sb.append(",zenDeviceEffectsUserModifiedFields=")
-                            .append(ZenDeviceEffects.fieldsToString(
-                                    zenDeviceEffectsUserModifiedFields));
-                }
-                if (deletionInstant != null) {
-                    sb.append(",deletionInstant=").append(deletionInstant);
-                }
-                if (Flags.modesUi()) {
-                    sb.append(",disabledOrigin=").append(disabledOrigin);
-                    sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
+                    .append(",condition=").append(condition)
+                    .append(",deviceEffects=").append(zenDeviceEffects)
+                    .append(",allowManualInvocation=").append(allowManualInvocation)
+                    .append(",iconResName=").append(iconResName)
+                    .append(",triggerDescription=").append(triggerDescription)
+                    .append(",type=").append(type);
+            if (userModifiedFields != 0) {
+                sb.append(",userModifiedFields=")
+                        .append(AutomaticZenRule.fieldsToString(userModifiedFields));
+            }
+            if (zenPolicyUserModifiedFields != 0) {
+                sb.append(",zenPolicyUserModifiedFields=")
+                        .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields));
+            }
+            if (zenDeviceEffectsUserModifiedFields != 0) {
+                sb.append(",zenDeviceEffectsUserModifiedFields=")
+                        .append(ZenDeviceEffects.fieldsToString(
+                                zenDeviceEffectsUserModifiedFields));
+            }
+            if (deletionInstant != null) {
+                sb.append(",deletionInstant=").append(deletionInstant);
+            }
+            if (Flags.modesUi()) {
+                sb.append(",disabledOrigin=").append(disabledOrigin);
+                sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
+                if (Flags.modesCleanupImplicit()) {
+                    sb.append(",lastActivation=").append(lastActivation);
                 }
             }
 
@@ -2869,7 +2832,7 @@
             proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime);
             proto.write(ZenRuleProto.ENABLED, enabled);
             proto.write(ZenRuleProto.ENABLER, enabler);
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE);
             } else {
                 proto.write(ZenRuleProto.IS_SNOOZING, snoozing);
@@ -2887,7 +2850,6 @@
             if (zenPolicy != null) {
                 zenPolicy.dumpDebug(proto, ZenRuleProto.ZEN_POLICY);
             }
-            proto.write(ZenRuleProto.MODIFIED, modified);
             proto.end(token);
         }
 
@@ -2908,26 +2870,25 @@
                     && Objects.equals(other.enabler, enabler)
                     && Objects.equals(other.zenPolicy, zenPolicy)
                     && Objects.equals(other.pkg, pkg)
-                    && other.modified == modified;
+                    && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
+                    && other.allowManualInvocation == allowManualInvocation
+                    && Objects.equals(other.iconResName, iconResName)
+                    && Objects.equals(other.triggerDescription, triggerDescription)
+                    && other.type == type
+                    && other.userModifiedFields == userModifiedFields
+                    && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields
+                    && other.zenDeviceEffectsUserModifiedFields
+                        == zenDeviceEffectsUserModifiedFields
+                    && Objects.equals(other.deletionInstant, deletionInstant);
 
-            if (Flags.modesApi()) {
+            if (Flags.modesUi()) {
                 finalEquals = finalEquals
-                        && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
-                        && other.allowManualInvocation == allowManualInvocation
-                        && Objects.equals(other.iconResName, iconResName)
-                        && Objects.equals(other.triggerDescription, triggerDescription)
-                        && other.type == type
-                        && other.userModifiedFields == userModifiedFields
-                        && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields
-                        && other.zenDeviceEffectsUserModifiedFields
-                            == zenDeviceEffectsUserModifiedFields
-                        && Objects.equals(other.deletionInstant, deletionInstant);
-
-                if (Flags.modesUi()) {
+                        && other.disabledOrigin == disabledOrigin
+                        && other.legacySuppressedEffects == legacySuppressedEffects
+                        && other.conditionOverride == conditionOverride;
+                if (Flags.modesCleanupImplicit()) {
                     finalEquals = finalEquals
-                            && other.disabledOrigin == disabledOrigin
-                            && other.legacySuppressedEffects == legacySuppressedEffects
-                            && other.conditionOverride == conditionOverride;
+                            && Objects.equals(other.lastActivation, lastActivation);
                 }
             }
 
@@ -2936,26 +2897,32 @@
 
         @Override
         public int hashCode() {
-            if (Flags.modesApi()) {
-                if (Flags.modesUi()) {
+            if (Flags.modesUi()) {
+                if (Flags.modesCleanupImplicit()) {
                     return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                             component, configurationActivity, pkg, id, enabler, zenPolicy,
-                            zenDeviceEffects, modified, allowManualInvocation, iconResName,
+                            zenDeviceEffects, allowManualInvocation, iconResName,
+                            triggerDescription, type, userModifiedFields,
+                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                            deletionInstant, disabledOrigin, legacySuppressedEffects,
+                            conditionOverride, lastActivation);
+                } else {
+                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+                            component, configurationActivity, pkg, id, enabler, zenPolicy,
+                            zenDeviceEffects, allowManualInvocation, iconResName,
                             triggerDescription, type, userModifiedFields,
                             zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
                             deletionInstant, disabledOrigin, legacySuppressedEffects,
                             conditionOverride);
-                } else {
-                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
-                            component, configurationActivity, pkg, id, enabler, zenPolicy,
-                            zenDeviceEffects, modified, allowManualInvocation, iconResName,
-                            triggerDescription, type, userModifiedFields,
-                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
-                            deletionInstant);
                 }
+            } else {
+                return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+                        component, configurationActivity, pkg, id, enabler, zenPolicy,
+                        zenDeviceEffects, allowManualInvocation, iconResName,
+                        triggerDescription, type, userModifiedFields,
+                        zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                        deletionInstant);
             }
-            return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
-                    component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
         }
 
         /** Returns a deep copy of the {@link ZenRule}. */
@@ -2971,7 +2938,7 @@
         }
 
         public boolean isActive() {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 if (!enabled || getPkg() == null) {
                     return false;
                 } else if (conditionOverride == OVERRIDE_ACTIVATE) {
@@ -2989,7 +2956,7 @@
         @VisibleForTesting(otherwise = VisibleForTesting.NONE)
         @ConditionOverride
         public int getConditionOverride() {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 return conditionOverride;
             } else {
                 return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE;
@@ -2997,7 +2964,7 @@
         }
 
         public void setConditionOverride(@ConditionOverride int value) {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 conditionOverride = value;
             } else {
                 if (value == OVERRIDE_ACTIVATE) {
@@ -3026,7 +2993,7 @@
          * manual deactivation (which used to be called "snoozing").
          */
         public void reconsiderConditionOverride() {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) {
                     resetConditionOverride();
                 } else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) {
@@ -3085,11 +3052,8 @@
                 & NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0;
         boolean allowConversations = (policy.priorityConversationSenders
                 & Policy.PRIORITY_CATEGORY_CONVERSATIONS) != 0;
-        boolean areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
-        if (Flags.modesApi()) {
-            areChannelsBypassingDnd = policy.hasPriorityChannels()
-                    && policy.allowPriorityChannels();
-        }
+        boolean areChannelsBypassingDnd =
+                policy.hasPriorityChannels() && policy.allowPriorityChannels();
         boolean allowSystem = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_SYSTEM) != 0;
         return !allowReminders && !allowCalls && !allowMessages && !allowEvents
                 && !allowRepeatCallers && !areChannelsBypassingDnd && !allowSystem
@@ -3129,15 +3093,12 @@
                     && !policy.isCategoryAllowed(PRIORITY_CATEGORY_EVENTS, false)
                     && !policy.isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false)
                     && !policy.isCategoryAllowed(PRIORITY_CATEGORY_SYSTEM, false)
-                    && !(config.areChannelsBypassingDnd && policy.getPriorityChannelsAllowed()
+                    && !(config.hasPriorityChannels && policy.getPriorityChannelsAllowed()
                     == STATE_ALLOW);
 
         } else {
-            boolean areChannelsBypassingDnd = config.areChannelsBypassingDnd;
-            if (Flags.modesApi()) {
-                areChannelsBypassingDnd = config.areChannelsBypassingDnd
-                        && config.isAllowPriorityChannels();
-            }
+            boolean areChannelsBypassingDnd = config.hasPriorityChannels
+                    && config.isAllowPriorityChannels();
             return !config.isAllowReminders() && !config.isAllowCalls() && !config.isAllowMessages()
                     && !config.isAllowEvents() && !config.isAllowRepeatCallers()
                     && !areChannelsBypassingDnd && !config.isAllowSystem();
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 31acd24..c159e40 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -16,7 +16,6 @@
 
 package android.service.notification;
 
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.app.Flags;
@@ -249,7 +248,7 @@
         public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom";
         public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom";
         public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects";
-        public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
+        public static final String FIELD_HAS_PRIORITY_CHANNELS = "hasPriorityChannels";
         public static final String FIELD_ALLOW_PRIORITY_CHANNELS = "allowPriorityChannels";
         private static final Set<String> PEOPLE_TYPE_FIELDS =
                 Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM);
@@ -323,15 +322,13 @@
                 addField(FIELD_SUPPRESSED_VISUAL_EFFECTS,
                         new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects));
             }
-            if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) {
-                addField(FIELD_ARE_CHANNELS_BYPASSING_DND,
-                        new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd));
+            if (from.hasPriorityChannels != to.hasPriorityChannels) {
+                addField(FIELD_HAS_PRIORITY_CHANNELS,
+                        new FieldDiff<>(from.hasPriorityChannels, to.hasPriorityChannels));
             }
-            if (Flags.modesApi()) {
-                if (from.allowPriorityChannels != to.allowPriorityChannels) {
-                    addField(FIELD_ALLOW_PRIORITY_CHANNELS,
-                            new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels));
-                }
+            if (from.allowPriorityChannels != to.allowPriorityChannels) {
+                addField(FIELD_ALLOW_PRIORITY_CHANNELS,
+                        new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels));
             }
 
             // Compare automatic and manual rules
@@ -491,7 +488,6 @@
         public static final String FIELD_ENABLER = "enabler";
         public static final String FIELD_ZEN_POLICY = "zenPolicy";
         public static final String FIELD_ZEN_DEVICE_EFFECTS = "zenDeviceEffects";
-        public static final String FIELD_MODIFIED = "modified";
         public static final String FIELD_PKG = "pkg";
         public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation";
         public static final String FIELD_ICON_RES = "iconResName";
@@ -532,7 +528,7 @@
             if (from.enabled != to.enabled) {
                 addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled));
             }
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 if (from.conditionOverride != to.conditionOverride) {
                     addField(FIELD_CONDITION_OVERRIDE,
                             new FieldDiff<>(from.conditionOverride, to.conditionOverride));
@@ -572,51 +568,40 @@
             if (!Objects.equals(from.enabler, to.enabler)) {
                 addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
             }
-            if (android.app.Flags.modesApi()) {
-                PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
-                if (policyDiff.hasDiff()) {
-                    addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
-                            policyDiff));
-                }
-            } else {
-                if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
-                    addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
-                }
-            }
-            if (from.modified != to.modified) {
-                addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
+            PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
+            if (policyDiff.hasDiff()) {
+                addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
+                        policyDiff));
             }
             if (!Objects.equals(from.pkg, to.pkg)) {
                 addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
             }
-            if (android.app.Flags.modesApi()) {
-                DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
-                        to.zenDeviceEffects);
-                if (deviceEffectsDiff.hasDiff()) {
-                    addField(FIELD_ZEN_DEVICE_EFFECTS,
-                            new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
-                                    deviceEffectsDiff));
-                }
-                if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
-                    addField(FIELD_TRIGGER_DESCRIPTION,
-                            new FieldDiff<>(from.triggerDescription, to.triggerDescription));
-                }
-                if (from.type != to.type) {
-                    addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
-                }
-                if (from.allowManualInvocation != to.allowManualInvocation) {
-                    addField(FIELD_ALLOW_MANUAL,
-                            new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
-                }
-                if (!Objects.equals(from.iconResName, to.iconResName)) {
-                    addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
-                }
-                if (android.app.Flags.modesUi()) {
-                    if (from.legacySuppressedEffects != to.legacySuppressedEffects) {
-                        addField(FIELD_LEGACY_SUPPRESSED_EFFECTS,
-                                new FieldDiff<>(from.legacySuppressedEffects,
-                                        to.legacySuppressedEffects));
-                    }
+            DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
+                    to.zenDeviceEffects);
+            if (deviceEffectsDiff.hasDiff()) {
+                addField(FIELD_ZEN_DEVICE_EFFECTS,
+                        new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
+                                deviceEffectsDiff));
+            }
+            if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
+                addField(FIELD_TRIGGER_DESCRIPTION,
+                        new FieldDiff<>(from.triggerDescription, to.triggerDescription));
+            }
+            if (from.type != to.type) {
+                addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
+            }
+            if (from.allowManualInvocation != to.allowManualInvocation) {
+                addField(FIELD_ALLOW_MANUAL,
+                        new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
+            }
+            if (!Objects.equals(from.iconResName, to.iconResName)) {
+                addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
+            }
+            if (android.app.Flags.modesUi()) {
+                if (from.legacySuppressedEffects != to.legacySuppressedEffects) {
+                    addField(FIELD_LEGACY_SUPPRESSED_EFFECTS,
+                            new FieldDiff<>(from.legacySuppressedEffects,
+                                    to.legacySuppressedEffects));
                 }
             }
         }
@@ -702,7 +687,6 @@
      * Diff class representing a change between two
      * {@link android.service.notification.ZenDeviceEffects}.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static class DeviceEffectsDiff extends BaseDiff {
         public static final String FIELD_GRAYSCALE = "mGrayscale";
         public static final String FIELD_SUPPRESS_AMBIENT_DISPLAY = "mSuppressAmbientDisplay";
@@ -843,7 +827,6 @@
     /**
      * Diff class representing a change between two {@link android.service.notification.ZenPolicy}.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static class PolicyDiff extends BaseDiff {
         public static final String FIELD_PRIORITY_CATEGORY_REMINDERS =
                 "mPriorityCategories_Reminders";
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 4cff67e..6b98c41 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -16,13 +16,11 @@
 
 package android.service.notification;
 
-import android.annotation.FlaggedApi;
 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;
 import android.os.Parcel;
@@ -78,91 +76,74 @@
      * the same time.
      * @hide
      */
-    @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
      */
-    @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
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_CONVERSATIONS = 1 << 2;
     /**
      * @hide
      */
-    @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
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15;
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16;
 
     private List<Integer> mPriorityCategories;
@@ -170,7 +151,6 @@
     private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
     private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
     private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
-    @FlaggedApi(Flags.FLAG_MODES_API)
     private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET;
 
     /** @hide */
@@ -358,7 +338,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int CHANNEL_POLICY_UNSET = 0;
 
     /**
@@ -367,7 +346,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int CHANNEL_POLICY_PRIORITY = 1;
 
     /**
@@ -376,7 +354,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static final int CHANNEL_POLICY_NONE = 2;
 
     /** @hide */
@@ -386,7 +363,6 @@
     }
 
     /** @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) {
@@ -409,7 +385,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static ZenPolicy getBasePolicyInterruptionFilterAlarms() {
         return new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -430,7 +405,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static ZenPolicy getBasePolicyInterruptionFilterNone() {
         return new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -628,7 +602,6 @@
      * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
      * with {@link NotificationChannel#canBypassDnd()} will be intercepted.
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @State int getPriorityChannelsAllowed() {
         switch (mAllowChannels) {
             case CHANNEL_POLICY_PRIORITY:
@@ -695,14 +668,10 @@
          * Builds the current ZenPolicy.
          */
         public @NonNull ZenPolicy build() {
-            if (Flags.modesApi()) {
-                return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories),
-                        new ArrayList<>(mZenPolicy.mVisualEffects),
-                        mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
-                        mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels);
-            } else {
-                return mZenPolicy.copy();
-            }
+            return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories),
+                    new ArrayList<>(mZenPolicy.mVisualEffects),
+                    mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
+                    mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels);
         }
 
         /**
@@ -1054,7 +1023,6 @@
          * Set whether priority channels are permitted to break through DND.
          */
         @SuppressLint("BuilderSetStyle")
-        @FlaggedApi(Flags.FLAG_MODES_API)
         public @NonNull Builder allowPriorityChannels(boolean allow) {
             mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
             return this;
@@ -1079,38 +1047,21 @@
         dest.writeInt(mPriorityMessages);
         dest.writeInt(mPriorityCalls);
         dest.writeInt(mConversationSenders);
-        if (Flags.modesApi()) {
-            dest.writeInt(mAllowChannels);
-        }
+        dest.writeInt(mAllowChannels);
     }
 
     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()
+                    return 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()
                         );
-                    } 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
@@ -1121,18 +1072,16 @@
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder(ZenPolicy.class.getSimpleName())
+        return new StringBuilder(ZenPolicy.class.getSimpleName())
                 .append('{')
                 .append("priorityCategories=[").append(priorityCategoriesToString())
                 .append("], visualEffects=[").append(visualEffectsToString())
                 .append("], priorityCallsSenders=").append(peopleTypeToString(mPriorityCalls))
                 .append(", priorityMessagesSenders=").append(peopleTypeToString(mPriorityMessages))
                 .append(", priorityConversationSenders=").append(
-                        conversationTypeToString(mConversationSenders));
-        if (Flags.modesApi()) {
-            sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels));
-        }
-        return sb.append('}').toString();
+                        conversationTypeToString(mConversationSenders))
+                .append(", allowChannels=").append(channelTypeToString(mAllowChannels))
+                .append('}').toString();
     }
 
     /** @hide */
@@ -1325,7 +1274,6 @@
     /**
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public static String channelTypeToString(@ChannelType int channelType) {
         switch (channelType) {
             case CHANNEL_POLICY_UNSET:
@@ -1344,25 +1292,18 @@
         if (o == this) return true;
         final ZenPolicy other = (ZenPolicy) o;
 
-        boolean eq = Objects.equals(other.mPriorityCategories, mPriorityCategories)
+        return Objects.equals(other.mPriorityCategories, mPriorityCategories)
                 && Objects.equals(other.mVisualEffects, mVisualEffects)
                 && other.mPriorityCalls == mPriorityCalls
                 && other.mPriorityMessages == mPriorityMessages
-                && other.mConversationSenders == mConversationSenders;
-        if (Flags.modesApi()) {
-            return eq && other.mAllowChannels == mAllowChannels;
-        }
-        return eq;
+                && other.mConversationSenders == mConversationSenders
+                && other.mAllowChannels == mAllowChannels;
     }
 
     @Override
     public int hashCode() {
-        if (Flags.modesApi()) {
-            return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
-                    mPriorityMessages, mConversationSenders, mAllowChannels);
-        }
-        return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
-                mConversationSenders);
+        return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
+                mPriorityMessages, mConversationSenders, mAllowChannels);
     }
 
     private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int
@@ -1480,13 +1421,10 @@
             }
         }
 
-        // apply allowed channels
-        if (Flags.modesApi()) {
-            // if no channels are allowed, can't newly allow them
-            if (mAllowChannels != CHANNEL_POLICY_NONE
-                    && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
-                mAllowChannels = policyToApply.mAllowChannels;
-            }
+        // apply allowed channels -> if no channels are allowed, can't newly allow them
+        if (mAllowChannels != CHANNEL_POLICY_NONE
+                && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
+            mAllowChannels = policyToApply.mAllowChannels;
         }
     }
 
@@ -1499,7 +1437,6 @@
      * @hide
      */
     @TestApi
-    @FlaggedApi(Flags.FLAG_MODES_API)
     public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) {
         ZenPolicy result = this.copy();
 
@@ -1596,10 +1533,7 @@
         proto.write(DNDPolicyProto.ALLOW_CALLS_FROM, getPriorityCallSenders());
         proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM, getPriorityMessageSenders());
         proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders());
-
-        if (Flags.modesApi()) {
-            proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed());
-        }
+        proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed());
 
         proto.flush();
         return bytes.toByteArray();
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 4647568..41a64e2 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -1595,8 +1595,17 @@
             mWindow.setSession(mSession);
 
             mLayout.packageName = getPackageName();
-            mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
-                    mCaller.getHandler());
+            if (com.android.server.display.feature.flags.Flags
+                    .displayListenerPerformanceImprovements()
+                    && com.android.server.display.feature.flags.Flags
+                    .committedStateSeparateEvent()) {
+                mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
+                        mCaller.getHandler(), DisplayManager.EVENT_TYPE_DISPLAY_CHANGED,
+                        DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED);
+            } else {
+                mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
+                        mCaller.getHandler());
+            }
             mDisplay = mIWallpaperEngine.mDisplay;
             // Use window context of TYPE_WALLPAPER so client can access UI resources correctly.
             mDisplayContext = createDisplayContext(mDisplay)
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index ca0959a..231aa68 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1599,7 +1599,6 @@
             mGlobal.registerDisplayListener(toRegister, executor,
                     DisplayManagerGlobal
                                     .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED
-                            | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE
                             | DisplayManagerGlobal
                                     .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED,
                     ActivityThread.currentPackageName());
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index ecdbaa3..d880072 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -44,6 +44,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.feature.flags.Flags;
 
 import java.util.Arrays;
 import java.util.Objects;
@@ -447,18 +448,20 @@
     }
 
     public boolean equals(DisplayInfo other) {
-        return equals(other, /* compareRefreshRate */ true);
+        return equals(other, /* compareOnlyBasicChanges */ false);
     }
 
     /**
      * Compares if the two DisplayInfo objects are equal or not
      * @param other The other DisplayInfo against which the comparison is to be done
-     * @param compareRefreshRate Indicates if the refresh rate is also to be considered in
-     *                           comparison
+     * @param compareOnlyBasicChanges Indicates if the changes to be compared are the ones which
+     *                               could lead to an emission of
+     *                    {@link android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED}
+     *                                event
      * @return
      */
-    public boolean equals(DisplayInfo other, boolean compareRefreshRate) {
-        boolean isEqualWithoutRefreshRate =  other != null
+    public boolean equals(DisplayInfo other, boolean compareOnlyBasicChanges) {
+        boolean isEqualWithOnlyBasicChanges =  other != null
                 && layerStack == other.layerStack
                 && flags == other.flags
                 && type == other.type
@@ -494,7 +497,6 @@
                 && physicalXDpi == other.physicalXDpi
                 && physicalYDpi == other.physicalYDpi
                 && state == other.state
-                && committedState == other.committedState
                 && ownerUid == other.ownerUid
                 && Objects.equals(ownerPackageName, other.ownerPackageName)
                 && removeMode == other.removeMode
@@ -512,14 +514,19 @@
                 thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId)
                 && canHostTasks == other.canHostTasks;
 
-        if (compareRefreshRate) {
-            return isEqualWithoutRefreshRate
+        if (!Flags.committedStateSeparateEvent()) {
+            isEqualWithOnlyBasicChanges = isEqualWithOnlyBasicChanges
+                    && (committedState == other.committedState);
+        }
+        if (!compareOnlyBasicChanges) {
+            return isEqualWithOnlyBasicChanges
                     && (getRefreshRate() == other.getRefreshRate())
                     && appVsyncOffsetNanos == other.appVsyncOffsetNanos
                     && presentationDeadlineNanos == other.presentationDeadlineNanos
-                    && (modeId == other.modeId);
+                    && (modeId == other.modeId)
+                    && (committedState == other.committedState);
         }
-        return isEqualWithoutRefreshRate;
+        return isEqualWithOnlyBasicChanges;
     }
 
     @Override
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 73cd5ec..df680c0 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -32,7 +32,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.TypedValue;
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 import android.widget.RemoteViews;
@@ -266,20 +265,14 @@
                 ? R.style.TextAppearance_DeviceDefault_Notification_Title
                 : R.style.TextAppearance_DeviceDefault_Notification_Info;
         // Most of the time, we're showing text in the minimized state
-        if (findViewById(R.id.header_text) instanceof TextView headerText) {
-            headerText.setTextAppearance(styleResId);
-            if (notificationsRedesignTemplates()) {
-                // TODO: b/378660052 - When inlining the redesign flag, this should be updated
-                //  directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to
-                //  override it here.
-                float textSize = getContext().getResources().getDimension(
-                        R.dimen.notification_2025_title_text_size);
-                headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
-            }
+        View headerText = findViewById(R.id.header_text);
+        if (headerText instanceof TextView) {
+            ((TextView) headerText).setTextAppearance(styleResId);
         }
         // If there's no summary or text, we show the app name instead of nothing
-        if (findViewById(R.id.app_name_text) instanceof TextView appNameText) {
-            appNameText.setTextAppearance(styleResId);
+        View appNameText = findViewById(R.id.app_name_text);
+        if (appNameText instanceof TextView) {
+            ((TextView) appNameText).setTextAppearance(styleResId);
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 80b4f2c..6b6147a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -118,6 +118,7 @@
 import static android.view.flags.Flags.disableDrawWakeLock;
 import static android.view.flags.Flags.sensitiveContentAppProtection;
 import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix;
+import static android.view.flags.Flags.toolkitFrameRateDebug;
 import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly;
 import static android.view.flags.Flags.toolkitFrameRateTouchBoost25q1;
 import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly;
@@ -538,6 +539,11 @@
     private static boolean sAlwaysAssignFocus;
 
     /**
+     * whether we pre-initialized the Buffer Allocator
+     */
+    private static boolean sPreInitializedBufferAllocator = false;
+
+    /**
      * This list must only be modified by the main thread.
      */
     final ArrayList<WindowCallbacks> mWindowCallbacks = new ArrayList<>();
@@ -1222,6 +1228,7 @@
             com.android.graphics.surfaceflinger.flags.Flags.vrrBugfix24q4();
     private static final boolean sEnableVrr = ViewProperties.vrr_enabled().orElse(true);
     private static final boolean sToolkitInitialTouchBoostFlagValue = toolkitInitialTouchBoost();
+    private static boolean sToolkitFrameRateDebugFlagValue =  toolkitFrameRateDebug();
 
     static {
         sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
@@ -1342,6 +1349,11 @@
                 com.android.server.display.feature.flags.Flags.subscribeGranularDisplayEvents();
 
         mSendPerfHintOnTouch = adpfViewrootimplActionDownBoost();
+
+        if (!sPreInitializedBufferAllocator) {
+            preInitBufferAllocator();
+            sPreInitializedBufferAllocator = true;
+        }
     }
 
     public static void addFirstDrawHandler(Runnable callback) {
@@ -13129,6 +13141,11 @@
                 if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
                     mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
                         frameRateCategory, false).applyAsyncUnsafe();
+
+                    if (sToolkitFrameRateDebugFlagValue) {
+                        Log.v(mTag, "### ViewRootImpl setFrameRateCategory '"
+                                + categoryToString(frameRateCategory) + "'");
+                    }
                 }
                 mLastPreferredFrameRateCategory = frameRateCategory;
             }
@@ -13191,8 +13208,15 @@
                     if (preferredFrameRate > 0) {
                         mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
                                 mFrameRateCompatibility);
+                        if (sToolkitFrameRateDebugFlagValue) {
+                            Log.v(mTag, "### ViewRootImpl setFrameRate '"
+                                    + preferredFrameRate + "'");
+                        }
                     } else {
                         mFrameRateTransaction.clearFrameRate(mSurfaceControl);
+                        if (sToolkitFrameRateDebugFlagValue) {
+                            Log.v(mTag, "### ViewRootImpl setFrameRate 0 Hz");
+                        }
                     }
                     mFrameRateTransaction.applyAsyncUnsafe();
                 }
@@ -13246,6 +13270,12 @@
             // mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName();
         }
         mDrawnThisFrame = true;
+
+        if (sToolkitFrameRateDebugFlagValue) {
+            String viewName = view == null ? "-" : view.getClass().getSimpleName();
+            Log.v(mTag, "### View: " + viewName +  " votes '"
+                    + categoryToString(frameRateCategory) + "'");
+        }
     }
 
     /**
@@ -13562,4 +13592,10 @@
             sProtoLogInitialized = true;
         }
     }
+
+    private void preInitBufferAllocator() {
+        if (com.android.graphics.hwui.flags.Flags.earlyPreinitBufferAllocator()) {
+            ThreadedRenderer.preInitBufferAllocator();
+        }
+    }
 }
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 578b7b6..ede0b3c 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -2684,9 +2684,10 @@
      * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection}
      * should be constructed with {@code this} node or a descendant of it.
      *
-     * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and {@link
-     * AccessibilityNodeInfo#setFocused} should both be called with {@code true} before setting the
-     * selection in order to make {@code this} node a candidate to contain a selection.
+     * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and
+     * {@link AccessibilityNodeInfo#setFocused} should both be called with {@code true}
+     * before setting the selection in order to make {@code this} node a candidate to
+     * contain a selection.
      *
      * <p><b>Note:</b> Cannot be called from an AccessibilityService. This class is made immutable
      * before being delivered to an AccessibilityService.
@@ -2706,12 +2707,10 @@
      * Gets the extended selection, which is a representation of selection that spans multiple nodes
      * that exist within the subtree of the node defining selection.
      *
-     * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection}
-     * should be constructed with {@code this} node or a descendant of it.
-     *
-     * <p><b>Note:</b> In order for a node to be a candidate to contain a selection, {@link
-     * AccessibilityNodeInfo#isFocusable()} ()} and {@link AccessibilityNodeInfo#isFocused()} should
-     * both be return with {@code true}.
+     * <p><b>Note:</b> Nodes that are candidates to contain a selection should return
+     * {@code true} from {@link #isFocusable()} and {@link #isFocused()}.
+     * The start and end {@link SelectionPosition}s of this {@link Selection}
+     * should exist within {@code this} node or its descendants.
      *
      * @return The extended selection within the node's subtree, or {@code null} if no selection
      *     exists.
@@ -5840,8 +5839,8 @@
         /**
          * Instantiates a new SelectionPosition.
          *
-         * @param view The {@link View} containing the virtual descendant associated with the
-         *     selection position.
+         * @param view The {@link View} containing the text associated with this selection
+         *     position.
          * @param offset The offset for a selection position within {@code view}'s text content,
          *     which should be a value between 0 and the length of {@code view}'s text.
          */
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 3bc2205..18fa0f3 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -143,4 +143,11 @@
     namespace: "toolkit"
     description: "Feature flag to update initial touch boost logic"
     bug: "393004744"
+}
+
+flag {
+    name: "toolkit_frame_rate_debug"
+    namespace: "toolkit"
+    description: "Feature flag to ennable ARR debug message"
+    bug: "394614443"
 }
\ No newline at end of file
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0f5476f..0a5c14e 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -8565,12 +8565,16 @@
                 return context;
             }
             try {
-                // Use PackageManager as the source of truth for application information, rather
-                // than the parceled ApplicationInfo provided by the app.
-                ApplicationInfo sanitizedApplication =
-                        context.getPackageManager().getApplicationInfoAsUser(
-                                mApplication.packageName, 0,
-                                UserHandle.getUserId(mApplication.uid));
+                ApplicationInfo sanitizedApplication = mApplication;
+                try {
+                    // Use PackageManager as the source of truth for application information, rather
+                    // than the parceled ApplicationInfo provided by the app.
+                    sanitizedApplication = context.getPackageManager().getApplicationInfoAsUser(
+                        mApplication.packageName, 0, UserHandle.getUserId(mApplication.uid));
+                } catch(SecurityException se) {
+                    Log.d(LOG_TAG, "Unable to fetch appInfo for " + mApplication.packageName);
+                }
+
                 Context applicationContext = context.createApplicationContext(
                         sanitizedApplication,
                         Context.CONTEXT_RESTRICTED);
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index d43469f..23da747 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -96,6 +96,7 @@
     ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
     ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
             true),
+    ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
     ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
     ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
     ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
@@ -114,6 +115,7 @@
     ENABLE_WINDOWING_SCALED_RESIZING(Flags::enableWindowingScaledResizing, true),
     ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
             Flags::enableWindowingTransitionHandlersObservers, false),
+    EXCLUDE_CAPTION_FROM_APP_BOUNDS(Flags::excludeCaptionFromAppBounds, false),
     INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(
             Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true)
     // go/keep-sorted end
diff --git a/core/java/android/window/SystemUiContext.java b/core/java/android/window/SystemUiContext.java
new file mode 100644
index 0000000..1e9a720
--- /dev/null
+++ b/core/java/android/window/SystemUiContext.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2025 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 android.window;
+
+import android.annotation.NonNull;
+import android.content.ComponentCallbacks;
+import android.content.ComponentCallbacksController;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+
+import com.android.window.flags.Flags;
+
+/**
+ * System Context to be used for UI. This Context has resources that can be themed.
+ *
+ * @see android.app.ActivityThread#getSystemUiContext(int)
+ *
+ * @hide
+ */
+public class SystemUiContext extends ContextWrapper implements ConfigurationDispatcher {
+
+    private final ComponentCallbacksController mCallbacksController =
+            new ComponentCallbacksController();
+
+    public SystemUiContext(Context base) {
+        super(base);
+        if (!Flags.trackSystemUiContextBeforeWms()) {
+            throw new UnsupportedOperationException("SystemUiContext can only be used after"
+                    + " flag is enabled.");
+        }
+    }
+
+    @Override
+    public void registerComponentCallbacks(@NonNull ComponentCallbacks callback) {
+        mCallbacksController.registerCallbacks(callback);
+    }
+
+    @Override
+    public void unregisterComponentCallbacks(@NonNull ComponentCallbacks callback) {
+        mCallbacksController.unregisterCallbacks(callback);
+    }
+
+    /** Dispatch {@link Configuration} to each {@link ComponentCallbacks}. */
+    @Override
+    public void dispatchConfigurationChanged(@NonNull Configuration newConfig) {
+        mCallbacksController.dispatchConfigurationChanged(newConfig);
+    }
+
+    @Override
+    public boolean shouldReportPrivateChanges() {
+        // We should report all config changes to update fields obtained from resources.
+        return true;
+    }
+}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index e358540..79120b2 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -79,6 +79,16 @@
 }
 
 flag {
+    name: "enable_drag_resize_set_up_in_bg_thread"
+    namespace: "lse_desktop_experience"
+    description: "Enables setting up the drag-resize input listener in a bg thread"
+    bug: "396445663"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_desktop_windowing_wallpaper_activity"
     namespace: "lse_desktop_experience"
     description: "Enables desktop wallpaper activity to show wallpaper in the desktop mode"
diff --git a/core/java/com/android/internal/app/MediaRouteControllerDialog.java b/core/java/com/android/internal/app/MediaRouteControllerDialog.java
index 61e63d1..621ec50 100644
--- a/core/java/com/android/internal/app/MediaRouteControllerDialog.java
+++ b/core/java/com/android/internal/app/MediaRouteControllerDialog.java
@@ -16,13 +16,10 @@
 
 package com.android.internal.app;
 
-import com.android.internal.R;
-
 import android.app.AlertDialog;
 import android.app.MediaRouteActionProvider;
 import android.app.MediaRouteButton;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.drawable.AnimationDrawable;
@@ -39,6 +36,8 @@
 import android.widget.LinearLayout;
 import android.widget.SeekBar;
 
+import com.android.internal.R;
+
 /**
  * This class implements the route controller dialog for {@link MediaRouter}.
  * <p>
@@ -60,7 +59,6 @@
     private final MediaRouterCallback mCallback;
     private final MediaRouter.RouteInfo mRoute;
 
-    private boolean mCreated;
     private Drawable mMediaRouteButtonDrawable;
     private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled };
     private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled };
@@ -102,31 +100,6 @@
     }
 
     /**
-     * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
-     *
-     * @return The media control view, or null if none.
-     */
-    public View getMediaControlView() {
-        return mControlView;
-    }
-
-    /**
-     * Sets whether to enable the volume slider and volume control using the volume keys
-     * when the route supports it.
-     * <p>
-     * The default value is true.
-     * </p>
-     */
-    public void setVolumeControlEnabled(boolean enable) {
-        if (mVolumeControlEnabled != enable) {
-            mVolumeControlEnabled = enable;
-            if (mCreated) {
-                updateVolume();
-            }
-        }
-    }
-
-    /**
      * Returns whether to enable the volume slider and volume control using the volume keys
      * when the route supports it.
      */
@@ -139,18 +112,15 @@
         setTitle(mRoute.getName());
         Resources res = getContext().getResources();
         setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect),
-                new OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialogInterface, int id) {
-                        if (mRoute.isSelected()) {
-                            if (mRoute.isBluetooth()) {
-                                mRouter.getDefaultRoute().select();
-                            } else {
-                                mRouter.getFallbackRoute().select();
-                            }
+                (dialogInterface, id) -> {
+                    if (mRoute.isSelected()) {
+                        if (mRoute.isBluetooth()) {
+                            mRouter.getDefaultRoute().select();
+                        } else {
+                            mRouter.getFallbackRoute().select();
                         }
-                        dismiss();
                     }
+                    dismiss();
                 });
         View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null);
         setView(customView, 0, 0, 0, 0);
@@ -160,8 +130,8 @@
         if (customPanelView != null) {
             customPanelView.setMinimumHeight(0);
         }
-        mVolumeLayout = (LinearLayout) customView.findViewById(R.id.media_route_volume_layout);
-        mVolumeSlider = (SeekBar) customView.findViewById(R.id.media_route_volume_slider);
+        mVolumeLayout = customView.findViewById(R.id.media_route_volume_layout);
+        mVolumeSlider = customView.findViewById(R.id.media_route_volume_slider);
         mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             private final Runnable mStopTrackingTouch = new Runnable() {
                 @Override
@@ -199,11 +169,10 @@
         });
 
         mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable();
-        mCreated = true;
         if (update()) {
             mControlView = onCreateMediaControlView(savedInstanceState);
             FrameLayout controlFrame =
-                    (FrameLayout) customView.findViewById(R.id.media_route_control_frame);
+                    customView.findViewById(R.id.media_route_control_frame);
             if (mControlView != null) {
                 controlFrame.addView(mControlView);
                 controlFrame.setVisibility(View.VISIBLE);
@@ -261,8 +230,7 @@
         Drawable icon = getIconDrawable();
         if (icon != mCurrentIconDrawable) {
             mCurrentIconDrawable = icon;
-            if (icon instanceof AnimationDrawable) {
-                AnimationDrawable animDrawable = (AnimationDrawable) icon;
+            if (icon instanceof AnimationDrawable animDrawable) {
                 if (!mAttachedToWindow && !mRoute.isConnecting()) {
                     // When the route is already connected before the view is attached, show the
                     // last frame of the connected animation immediately.
diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS
index 52f18fb..0bba24d 100644
--- a/core/java/com/android/internal/app/OWNERS
+++ b/core/java/com/android/internal/app/OWNERS
@@ -20,3 +20,6 @@
 
 # System language settings
 per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
+
+# Media
+per-file *MediaRoute* = file:/packages/SystemUI/src/com/android/systemui/media/dialog/OWNERS
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 3750076..ac186d0 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -56,8 +56,7 @@
     private static final int HASH_SIZE_BYTES = 32;
 
     public static boolean isFsVeritySupported() {
-        return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
-                || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
+        return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R;
     }
 
     /** Enables fs-verity for the file without signature. */
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 0ec55f9..1f90760 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -87,10 +87,10 @@
     private static final int CELL_ACTIVATE = 0;
     private static final int CELL_DEACTIVATE = 1;
 
-    private final int mDotSize;
-    private final int mDotSizeActivated;
+    private int mDotSize;
+    private int mDotSizeActivated;
     private final float mDotHitFactor;
-    private final int mPathWidth;
+    private int mPathWidth;
     private final int mLineFadeOutAnimationDurationMs;
     private final int mLineFadeOutAnimationDelayMs;
     private final int mFadePatternAnimationDurationMs;
@@ -1341,6 +1341,38 @@
         invalidate();
     }
 
+    /**
+     * Change dot colors
+     */
+    public void setDotColors(int dotColor, int dotActivatedColor) {
+        mDotColor = dotColor;
+        mDotActivatedColor = dotActivatedColor;
+        invalidate();
+    }
+
+    /**
+     * Keeps dot activated until the next dot gets activated.
+     */
+    public void setKeepDotActivated(boolean keepDotActivated) {
+        mKeepDotActivated = keepDotActivated;
+    }
+
+    /**
+     * Set dot sizes in dp
+     */
+    public void setDotSizes(int dotSizeDp, int dotSizeActivatedDp) {
+        mDotSize = dotSizeDp;
+        mDotSizeActivated = dotSizeActivatedDp;
+    }
+
+    /**
+     * Set the stroke width of the pattern line.
+     */
+    public void setPathWidth(int pathWidthDp) {
+        mPathWidth = pathWidthDp;
+        mPathPaint.setStrokeWidth(mPathWidth);
+    }
+
     private float getCenterXForColumn(int column) {
         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
     }
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 2ba6bc4..b679688 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -664,14 +664,16 @@
 
 static jint android_media_AudioSystem_setDeviceConnectionState(JNIEnv *env, jobject thiz,
                                                                jint state, jobject jParcel,
-                                                               jint codec) {
+                                                               jint codec, jboolean deviceSwitch) {
     int status;
     if (Parcel *parcel = parcelForJavaObject(env, jParcel); parcel != nullptr) {
         android::media::audio::common::AudioPort port{};
         if (status_t statusOfParcel = port.readFromParcel(parcel); statusOfParcel == OK) {
-        status = check_AudioSystem_Command(
-                AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(state),
-                                                      port, static_cast<audio_format_t>(codec)));
+            status = check_AudioSystem_Command(
+                    AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(
+                                                                  state),
+                                                          port, static_cast<audio_format_t>(codec),
+                                                          deviceSwitch));
         } else {
             ALOGE("Failed to read from parcel: %s", statusToString(statusOfParcel).c_str());
             status = kAudioStatusError;
@@ -3457,7 +3459,7 @@
         MAKE_AUDIO_SYSTEM_METHOD(newAudioSessionId),
         MAKE_AUDIO_SYSTEM_METHOD(newAudioPlayerId),
         MAKE_AUDIO_SYSTEM_METHOD(newAudioRecorderId),
-        MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;I)I",
+        MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;IZ)I",
                                android_media_AudioSystem_setDeviceConnectionState),
         MAKE_AUDIO_SYSTEM_METHOD(getDeviceConnectionState),
         MAKE_AUDIO_SYSTEM_METHOD(handleDeviceConfigChange),
diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
index e0cc055..c4259f4 100644
--- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
+++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
@@ -266,16 +266,24 @@
   }
   // Picky version of atoi(). No sign or unexpected characters allowed. Return -1 on failure.
   static int digitsVal(char* start, char* end) {
+    constexpr int vmax = std::numeric_limits<int>::max();
     int result = 0;
-    if (end - start > 6) {
-      return -1;
-    }
     for (char* dp = start; dp < end; ++dp) {
       if (*dp < '0' || *dp > '9') {
-        ALOGW("Argument failed integer format check");
+        ALOGW("Argument contains non-integer characters");
         return -1;
       }
-      result = 10 * result + (*dp - '0');
+      int digit = *dp - '0';
+      if (result > vmax / 10) {
+        ALOGW("Argument exceeds int limit");
+        return -1;
+      }
+      result *= 10;
+      if (result > vmax - digit) {
+        ALOGW("Argument exceeds int limit");
+        return -1;
+      }
+      result += digit;
     }
     return result;
   }
diff --git a/core/res/Android.bp b/core/res/Android.bp
index be4fb8b..1199d77 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -174,6 +174,7 @@
         "android.media.tv.flags-aconfig",
         "android.security.flags-aconfig",
         "device_policy_aconfig_flags",
+        "android.xr.flags-aconfig",
         "com.android.hardware.input.input-aconfig",
         "aconfig_trade_in_mode_flags",
         "art-aconfig-flags",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6f70d88..78526ad 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5230,6 +5230,182 @@
         android:protectionLevel="signature|privileged" />
 
     <!-- ==================================== -->
+    <!-- Permissions for XR perception data   -->
+    <!-- ==================================== -->
+    <eat-comment />
+
+    <!-- Used for permissions that are associated with accessing XR
+         tracked information about the person using the device and the
+         environment around them.
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission-group android:name="android.permission-group.XR_TRACKING"
+                      android:label="@string/permgrouplab_xr_tracking"
+                      android:description="@string/permgroupdesc_xr_tracking"
+                      android:priority="100"
+                      android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get approximate eye gaze.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.EYE_TRACKING_COARSE"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_eye_tracking_coarse"
+                android:description="@string/permdesc_eye_tracking_coarse"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get face tracking data.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.FACE_TRACKING"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_face_tracking"
+                android:description="@string/permdesc_face_tracking"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get hand tracking data.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.HAND_TRACKING"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_hand_tracking"
+                android:description="@string/permdesc_hand_tracking"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get data derived by sensing the
+         user's environment.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.SCENE_UNDERSTANDING_COARSE"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:description="@string/permdesc_scene_understanding_coarse"
+                android:label="@string/permlab_scene_understanding_coarse"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Used for permissions that are associated with accessing
+         particularly sensitive XR tracking data.
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission-group android:name="android.permission-group.XR_TRACKING_SENSITIVE"
+                      android:label="@string/permgrouplab_xr_tracking_sensitive"
+                      android:description="@string/permgroupdesc_xr_tracking_sensitive"
+                      android:priority="100"
+                      android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get precise eye gaze data.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.EYE_TRACKING_FINE"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_eye_tracking_fine"
+                android:description="@string/permdesc_eye_tracking_fine"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get head tracking data.  Unmanaged
+         activities (OpenXR activities with the manifest property
+         "android.window.PROPERTY_XR_ACTIVITY_START_MODE" set to
+         "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED") do not require
+         this permission to get head tracking data.
+
+         {@see https://developer.android.com/develop/xr/get-started#property_activity_xr_start_mode_property}
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.HEAD_TRACKING"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:label="@string/permlab_head_tracking"
+                android:description="@string/permdesc_head_tracking"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to get highly precise data derived by sensing the
+         user's environment, such as a depth map.
+
+         <p>Protection level: dangerous
+
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+    <permission android:name="android.permission.SCENE_UNDERSTANDING_FINE"
+                android:protectionLevel="dangerous"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:description="@string/permdesc_scene_understanding_fine"
+                android:label="@string/permlab_scene_understanding_fine"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to trigger Eye Calibration, which
+         calibrates for IPD (inter-pupillary distance) adjustment and
+         eye tracking.
+
+         <p>Protection level: signature|privileged
+
+         @SystemApi
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+         @hide -->
+    <permission android:name="android.permission.EYE_CALIBRATION"
+                android:protectionLevel="signature|privileged"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to trigger Face Tracking Calibration.
+
+         <p>Protection level: signature|privileged
+
+         @SystemApi
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+         @hide -->
+    <permission android:name="android.permission.FACE_TRACKING_CALIBRATION"
+                android:protectionLevel="signature|privileged"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to import an anchor created and
+         exported by another application.
+
+         <p>Protection level: signature|privileged
+
+         @SystemApi
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+         @hide -->
+    <permission android:name="android.permission.IMPORT_XR_ANCHOR"
+                android:protectionLevel="signature|privileged"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- Allows an application to access XR tracking data while in the
+         background. Without this permission, XR tracking data such as
+         head tracking, hand tracking, eye tracking, or face tracking
+         is only available to an activity it is in the
+         foreground. With this permission, such data is also available
+         to services and to activities that are in the background.
+
+         <p>This permission must be granted in addition to the
+         corresponding permission such as {@link #HEAD_TRACKING} or
+         {@link #FACE_TRACKING} for the data being accessed.
+
+         <p>Protection level: normal|appop
+
+         @SystemApi
+         @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+         @hide -->
+    <permission android:name="android.permission.XR_TRACKING_IN_BACKGROUND"
+                android:protectionLevel="normal|appop"
+                android:description="@string/permdesc_xr_tracking_in_background"
+                android:label="@string/permlab_xr_tracking_in_background"
+                android:featureFlag="android.xr.xr_manifest_entries" />
+
+    <!-- ==================================== -->
     <!-- Private permissions                  -->
     <!-- ==================================== -->
     <eat-comment />
diff --git a/core/res/res/drawable-w192dp/loader_horizontal_watch.xml b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml
new file mode 100644
index 0000000..18cea6e
--- /dev/null
+++ b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml
@@ -0,0 +1,97 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="15dp" android:width="67dp" android:viewportHeight="15" android:viewportWidth="67">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="33.5" android:translateY="7.5">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M33.5 -7.5 C33.5,-7.5 33.5,7.5 33.5,7.5 C33.5,7.5 -33.5,7.5 -33.5,7.5 C-33.5,7.5 -33.5,-7.5 -33.5,-7.5 C-33.5,-7.5 33.5,-7.5 33.5,-7.5c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-296.5" android:translateY="-62.5" android:pivotX="330" android:pivotY="70" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-224.84700000000004" android:translateY="-321.948" android:pivotX="555.09" android:pivotY="-329" android:rotation="28.9" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M194.88 359 C190,359 185.05,357.81 180.48,355.3 C59.86,289.14 -41.55,191.9 -112.79,74.11 C-186.14,-47.16 -224.91,-186.55 -224.91,-329 C-224.91,-345.57 -211.48,-359 -194.91,-359 C-178.34,-359 -164.91,-345.57 -164.91,-329 C-164.91,-197.5 -129.13,-68.84 -61.45,43.06 C4.33,151.82 97.97,241.6 209.33,302.69 C223.86,310.66 229.18,328.9 221.21,343.42 C215.75,353.37 205.48,359 194.88,359c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="744.323" android:translateY="-277.96299999999997" android:pivotX="-414.08" android:pivotY="-372.985" android:rotation="28.9">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-335.95 402.99 C-351.13,402.99 -364.16,391.5 -365.76,376.07 C-367.46,359.59 -355.49,344.85 -339.01,343.14 C-162.93,324.91 -0.15,242.33 119.34,110.62 C239.66,-22.01 305.92,-193.76 305.92,-372.98 C305.92,-389.55 319.35,-402.98 335.92,-402.98 C352.49,-402.98 365.92,-389.55 365.92,-372.98 C365.92,-178.82 294.13,7.24 163.78,150.93 C34.34,293.61 -142.03,383.07 -332.83,402.82 C-333.88,402.93 -334.92,402.99 -335.95,402.99c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="185.385" android:translateY="70.09100000000001" android:pivotX="144.858" android:pivotY="-721.039" android:rotation="28.9" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M144.62 58.96 C144.61,58.96 144.61,58.96 144.6,58.96 C40.39,58.93 -60.82,38.66 -156.19,-1.28 C-171.48,-7.68 -178.68,-25.26 -172.28,-40.54 C-165.88,-55.82 -148.3,-63.02 -133.02,-56.62 C-45.02,-19.77 48.4,-1.07 144.63,-1.04 C161.19,-1.03 174.62,12.4 174.62,28.97 C174.61,45.53 161.18,58.96 144.62,58.96c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="330" android:translateY="70">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-660 -313 C-660,-313 -660,313 -660,313 C-660,313 660,313 660,313 C660,313 660,-313 660,-313 C660,-313 -660,-313 -660,-313c  M300.74 -1.16 C205.46,38.62 103.22,59.09 -0.03,59.05 C-103.28,59.01 -205.51,38.48 -300.76,-1.37 C-316.05,-7.76 -323.26,-25.34 -316.86,-40.62 C-310.47,-55.91 -292.9,-63.12 -277.61,-56.72 C-189.68,-19.94 -95.32,-0.98 -0.01,-0.95 C95.3,-0.92 189.67,-19.81 277.63,-56.53 C292.92,-62.91 310.49,-55.69 316.87,-40.4 C323.25,-25.11 316.03,-7.54 300.74,-1.16c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable-w204dp/loader_horizontal_watch.xml b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml
new file mode 100644
index 0000000..fbc6eab
--- /dev/null
+++ b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml
@@ -0,0 +1,104 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="15dp" android:width="70dp" android:viewportHeight="15" android:viewportWidth="70">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="35" android:translateY="7.5">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M35 -7.5 C35,-7.5 35,7.5 35,7.5 C35,7.5 -35,7.5 -35,7.5 C-35,7.5 -35,-7.5 -35,-7.5 C-35,-7.5 35,-7.5 35,-7.5c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-310" android:translateY="-64" android:pivotX="345" android:pivotY="71.5" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-239.44799999999998" android:translateY="-341.45" android:pivotX="584.448" android:pivotY="-346.55" android:rotation="28.8" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M205.28 376.55 C200.4,376.55 195.46,375.36 190.88,372.85 C64.08,303.29 -42.54,201.07 -117.44,77.24 C-194.55,-50.25 -235.31,-196.79 -235.31,-346.55 C-235.31,-363.12 -221.88,-376.55 -205.31,-376.55 C-188.74,-376.55 -175.31,-363.12 -175.31,-346.55 C-175.31,-207.74 -137.54,-71.93 -66.1,46.19 C3.34,160.99 102.18,255.76 219.73,320.24 C234.26,328.21 239.58,346.45 231.61,360.97 C226.15,370.92 215.88,376.55 205.28,376.55c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="781.413" android:translateY="-295.124" android:pivotX="-436.413" android:pivotY="-392.876" android:rotation="28.8">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-353.86 422.88 C-369.04,422.88 -382.07,411.4 -383.67,395.97 C-385.37,379.49 -373.4,364.74 -356.92,363.03 C-171.06,343.79 0.76,256.62 126.89,117.59 C253.89,-22.41 323.83,-203.7 323.83,-392.88 C323.83,-409.44 337.26,-422.88 353.83,-422.88 C370.4,-422.88 383.83,-409.44 383.83,-392.88 C383.83,-188.76 308.36,6.84 171.32,157.9 C35.25,307.89 -150.15,401.94 -350.74,422.72 C-351.79,422.82 -352.83,422.88 -353.86,422.88c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="192.671" android:translateY="71.49599999999998" android:pivotX="152.329" android:pivotY="-759.496" android:rotation="28.8" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M152.33 60.5 C152.33,60.5 152.32,60.5 152.32,60.5 C42.76,60.47 -63.64,39.16 -163.91,-2.82 C-179.19,-9.22 -186.39,-26.8 -179.99,-42.08 C-173.59,-57.36 -156.02,-64.57 -140.73,-58.16 C-47.84,-19.27 50.77,0.47 152.34,0.5 C168.91,0.51 182.33,13.94 182.33,30.51 C182.32,47.08 168.89,60.5 152.33,60.5c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="345" android:translateY="71.5">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-579 -259.5 C-579,-259.5 -579,259.5 -579,259.5 C-579,259.5 579,259.5 579,259.5 C579,259.5 579,-259.5 579,-259.5 C579,-259.5 -579,-259.5 -579,-259.5c  M316.17 -2.8 C216,39.02 108.52,60.54 -0.03,60.5 C-108.58,60.46 -216.04,38.87 -316.18,-3.02 C-331.47,-9.41 -338.68,-26.99 -332.28,-42.27 C-325.89,-57.56 -308.32,-64.76 -293.03,-58.37 C-200.22,-19.55 -100.61,0.46 -0.01,0.5 C100.6,0.54 200.22,-19.41 293.06,-58.17 C308.35,-64.55 325.92,-57.33 332.3,-42.04 C338.68,-26.75 331.46,-9.18 316.17,-2.8c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable-w216dp/loader_horizontal_watch.xml b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml
new file mode 100644
index 0000000..ed4b7ea
--- /dev/null
+++ b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml
@@ -0,0 +1,105 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="16dp" android:width="74dp" android:viewportHeight="16" android:viewportWidth="74">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="37" android:translateY="8">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M37 -8 C37,-8 37,8 37,8 C37,8 -37,8 -37,8 C-37,8 -37,-8 -37,-8 C-37,-8 37,-8 37,-8c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-328" android:translateY="-65.5" android:pivotX="365" android:pivotY="73.5" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-256.447" android:translateY="-365.014" android:pivotX="621.447" android:pivotY="-368.486" android:rotation="28.8" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M218.28 398.49 C213.4,398.49 208.46,397.3 203.88,394.78 C69.34,320.99 -43.78,212.53 -123.25,81.15 C-205.06,-54.11 -248.31,-209.59 -248.31,-368.49 C-248.31,-385.05 -234.88,-398.49 -218.31,-398.49 C-201.74,-398.49 -188.31,-385.05 -188.31,-368.49 C-188.31,-220.54 -148.06,-75.8 -71.91,50.09 C2.1,172.45 107.45,273.45 232.73,342.18 C247.26,350.15 252.58,368.38 244.61,382.91 C239.15,392.86 228.88,398.49 218.28,398.49c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="829.0260000000001" android:translateY="-315.759" android:pivotX="-464.026" android:pivotY="-417.741" android:rotation="28.8">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-376.25 447.74 C-391.43,447.74 -404.46,436.26 -406.05,420.83 C-407.76,404.35 -395.78,389.61 -379.3,387.9 C-181.22,367.38 1.9,274.48 136.32,126.3 C271.67,-22.9 346.22,-216.12 346.22,-417.74 C346.22,-434.31 359.65,-447.74 376.22,-447.74 C392.79,-447.74 406.22,-434.31 406.22,-417.74 C406.22,-201.18 326.15,6.35 180.76,166.61 C36.39,325.75 -160.31,425.54 -373.12,447.58 C-374.17,447.69 -375.22,447.74 -376.25,447.74c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="203.029" android:translateY="74.06899999999996" android:pivotX="161.971" android:pivotY="-807.569" android:rotation="28.8" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M161.97 62.43 C161.97,62.43 161.96,62.43 161.96,62.43 C45.71,62.4 -67.17,39.79 -173.55,-4.75 C-188.83,-11.15 -196.03,-28.72 -189.63,-44.01 C-183.24,-59.29 -165.66,-66.49 -150.38,-60.09 C-51.37,-18.64 53.72,2.4 161.98,2.43 C178.55,2.44 191.98,15.87 191.97,32.44 C191.97,49 178.54,62.43 161.97,62.43c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="365" android:translateY="73.5">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-609 -244.5 C-609,-244.5 -609,244.5 -609,244.5 C-609,244.5 609,244.5 609,244.5 C609,244.5 609,-244.5 609,-244.5 C609,-244.5 -609,-244.5 -609,-244.5c  M335.44 -4.16 C229.17,40.21 115.13,63.04 -0.04,63 C-115.21,62.96 -229.22,40.05 -335.47,-4.39 C-350.76,-10.79 -357.95,-28.36 -351.56,-43.65 C-345.17,-58.93 -327.59,-66.14 -312.31,-59.74 C-213.39,-18.36 -107.24,2.96 -0.02,3 C107.21,3.04 213.38,-18.22 312.33,-59.53 C327.62,-65.91 345.19,-58.69 351.57,-43.4 C357.95,-28.11 350.73,-10.54 335.44,-4.16c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
+
+
diff --git a/core/res/res/drawable-w228dp/loader_horizontal_watch.xml b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml
new file mode 100644
index 0000000..6b86c63
--- /dev/null
+++ b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml
@@ -0,0 +1,103 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c  M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/core/res/res/drawable-w240dp/loader_horizontal_watch.xml b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml
new file mode 100644
index 0000000..ad60bbd
--- /dev/null
+++ b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml
@@ -0,0 +1,104 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="17dp" android:width="82dp" android:viewportHeight="17" android:viewportWidth="82">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="41" android:translateY="8.5">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M41 -8.5 C41,-8.5 41,8.5 41,8.5 C41,8.5 -41,8.5 -41,8.5 C-41,8.5 -41,-8.5 -41,-8.5 C-41,-8.5 41,-8.5 41,-8.5c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-362.5" android:translateY="-69" android:pivotX="403.5" android:pivotY="77.5" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-291.64799999999997" android:translateY="-414.141" android:pivotX="695.448" android:pivotY="-412.359" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M244.28 442.36 C239.4,442.36 234.45,441.17 229.88,438.66 C79.87,356.38 -46.26,235.46 -134.87,88.96 C-179.66,14.91 -214.28,-64.74 -237.78,-147.79 C-262.02,-233.47 -274.31,-322.49 -274.31,-412.36 C-274.31,-428.93 -260.88,-442.36 -244.31,-442.36 C-227.74,-442.36 -214.31,-428.93 -214.31,-412.36 C-214.31,-328.01 -202.78,-244.49 -180.05,-164.12 C-158.01,-86.24 -125.54,-11.54 -83.53,57.91 C-0.38,195.38 117.97,308.85 258.73,386.05 C273.26,394.02 278.58,412.26 270.61,426.78 C265.15,436.73 254.88,442.36 244.28,442.36c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="923.0530000000001" android:translateY="-359.029" android:pivotX="-519.253" android:pivotY="-467.471" android:rotation="28.7">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-421.02 497.47 C-436.2,497.47 -449.23,485.99 -450.83,470.56 C-452.53,454.08 -440.56,439.34 -424.08,437.63 C-201.54,414.57 4.18,310.19 155.19,143.73 C229.54,61.77 287.7,-31.75 328.04,-134.22 C369.81,-240.3 390.99,-352.42 390.99,-467.47 C390.99,-484.04 404.42,-497.47 420.99,-497.47 C437.56,-497.47 450.99,-484.04 450.99,-467.47 C450.99,-226.02 361.72,5.35 199.63,184.04 C38.67,361.47 -180.63,472.73 -417.89,497.31 C-418.94,497.42 -419.99,497.47 -421.02,497.47c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="222.54600000000002" android:translateY="77.21400000000006" android:pivotX="181.254" android:pivotY="-903.714" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M181.26 66.28 C181.25,66.28 181.25,66.28 181.25,66.28 C51.64,66.25 -74.22,41.06 -192.83,-8.6 C-208.12,-15 -215.32,-32.58 -208.92,-47.86 C-202.52,-63.15 -184.94,-70.35 -169.66,-63.95 C-58.42,-17.38 59.64,6.25 181.26,6.28 C197.83,6.29 211.26,19.72 211.26,36.29 C211.25,52.86 197.82,66.28 181.26,66.28c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="403.5" android:translateY="77.5">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-630.5 -255.5 C-630.5,-255.5 -630.5,255.5 -630.5,255.5 C-630.5,255.5 630.5,255.5 630.5,255.5 C630.5,255.5 630.5,-255.5 630.5,-255.5 C630.5,-255.5 -630.5,-255.5 -630.5,-255.5c  M374 -8.88 C255.5,40.59 128.4,66.04 0,66 C-128.4,65.95 -255.6,40.42 -374,-9.14 C-389.3,-15.53 -396.5,-33.11 -390.1,-48.39 C-383.7,-63.68 -366.2,-70.88 -350.9,-64.49 C-239.7,-18 -120.5,5.96 0,6 C120.4,6.04 239.7,-17.84 350.9,-64.25 C366.2,-70.63 383.7,-63.41 390.1,-48.12 C396.5,-32.83 389.3,-15.26 374,-8.88c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable/ic_notification_summarization.xml b/core/res/res/drawable/ic_notification_summarization.xml
index de905fa..d476872 100644
--- a/core/res/res/drawable/ic_notification_summarization.xml
+++ b/core/res/res/drawable/ic_notification_summarization.xml
@@ -19,5 +19,6 @@
     android:tint="?android:attr/colorControlNormal"
     android:viewportHeight="960"
     android:viewportWidth="960">
-    <path android:fillColor="#ffffff" android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
+    <path android:fillColor="#ffffff"
+          android:pathData="M120,840L120,760L600,760L600,840L120,840ZM120,640L120,560L840,560L840,640L120,640ZM120,440L120,360L560,360L560,440L120,440ZM700,480Q700,388 636,324Q572,260 480,260Q572,260 636,196Q700,132 700,40Q700,132 764,196Q828,260 920,260Q828,260 764,324Q700,388 700,480Z"/>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/loader_horizontal_watch.xml b/core/res/res/drawable/loader_horizontal_watch.xml
new file mode 100644
index 0000000..6b86c63
--- /dev/null
+++ b/core/res/res/drawable/loader_horizontal_watch.xml
@@ -0,0 +1,103 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8">
+                    <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/>
+                </group>
+                <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1">
+                    <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7">
+                        <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0">
+                        <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/>
+                    </group>
+                    <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75">
+                        <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c  M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_6_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_5_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml
index 75bd244..1bde173 100644
--- a/core/res/res/layout/notification_2025_conversation_header.xml
+++ b/core/res/res/layout/notification_2025_conversation_header.xml
@@ -29,7 +29,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-        android:textSize="@dimen/notification_2025_title_text_size"
+        android:textSize="16sp"
         android:singleLine="true"
         android:layout_weight="1"
         />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index 05458329..d29b7af 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -102,7 +102,6 @@
                     android:singleLine="true"
                     android:textAlignment="viewStart"
                     android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-                    android:textSize="@dimen/notification_2025_title_text_size"
                     />
 
                 <include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 9959b66..5beab50 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -104,7 +104,6 @@
                     android:singleLine="true"
                     android:textAlignment="viewStart"
                     android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-                    android:textSize="@dimen/notification_2025_title_text_size"
                     />
 
                 <include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index 85ca124..d7c3263 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -130,7 +130,6 @@
                             android:singleLine="true"
                             android:textAlignment="viewStart"
                             android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-                            android:textSize="@dimen/notification_2025_title_text_size"
                             />
 
                         <include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
index 11fc486..52bc7b8 100644
--- a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
@@ -69,7 +69,6 @@
                 android:singleLine="true"
                 android:textAlignment="viewStart"
                 android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-                android:textSize="@dimen/notification_2025_title_text_size"
                 />
             <include layout="@layout/notification_2025_top_line_views" />
         </NotificationTopLineView>
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
index bf70a5e..cf9ff6b 100644
--- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
@@ -90,7 +90,6 @@
                 android:singleLine="true"
                 android:textAlignment="viewStart"
                 android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
-                android:textSize="@dimen/notification_2025_title_text_size"
                 />
             <include layout="@layout/notification_2025_top_line_views" />
         </NotificationTopLineView>
diff --git a/core/res/res/values-w192dp/dimens_watch.xml b/core/res/res/values-w192dp/dimens_watch.xml
new file mode 100644
index 0000000..c6bf767
--- /dev/null
+++ b/core/res/res/values-w192dp/dimens_watch.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+<resources>
+    <!-- 16.7% of display size -->
+    <dimen name="base_error_dialog_top_padding">32dp</dimen>
+    <!-- 5.2% of display size -->
+    <dimen name="base_error_dialog_padding">10dp</dimen>
+    <!-- 20.83% of display size -->
+    <dimen name="base_error_dialog_bottom_padding">40dp</dimen>
+
+    <!--  watch's indeterminate progress bar dimens based on the current screen size -->
+    <dimen name="loader_horizontal_min_width_watch">67dp</dimen>
+    <dimen name="loader_horizontal_min_height_watch">15dp</dimen>
+</resources>
diff --git a/core/res/res/values-w204dp-round-watch/dimens_watch.xml b/core/res/res/values-w204dp-round-watch/dimens_watch.xml
new file mode 100644
index 0000000..3509474
--- /dev/null
+++ b/core/res/res/values-w204dp-round-watch/dimens_watch.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+<resources>
+    <!--  watch's indeterminate progress bar dimens based on the current screen size -->
+    <dimen name="loader_horizontal_min_width_watch">70dp</dimen>
+    <dimen name="loader_horizontal_min_height_watch">15dp</dimen>
+</resources>
diff --git a/core/res/res/values-w216dp/dimens_watch.xml b/core/res/res/values-w216dp/dimens_watch.xml
new file mode 100644
index 0000000..e14ce5e
--- /dev/null
+++ b/core/res/res/values-w216dp/dimens_watch.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<resources>
+    <!--  watch's indeterminate progress bar dimens based on the current screen size -->
+    <dimen name="loader_horizontal_min_width_watch">72dp</dimen>
+    <dimen name="loader_horizontal_min_height_watch">16dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/res/res/values-w228dp/dimens_watch.xml b/core/res/res/values-w228dp/dimens_watch.xml
new file mode 100644
index 0000000..3c62656
--- /dev/null
+++ b/core/res/res/values-w228dp/dimens_watch.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+<resources>
+    <!--  watch's indeterminate progress bar dimens based on the current screen size -->
+    <dimen name="loader_horizontal_min_width">76dp</dimen>
+    <dimen name="loader_horizontal_min_height">14dp</dimen>
+</resources>
diff --git a/core/res/res/values-w240dp/dimens_material.xml b/core/res/res/values-w240dp/dimens_material.xml
index bd26c8b..e30aea4 100644
--- a/core/res/res/values-w240dp/dimens_material.xml
+++ b/core/res/res/values-w240dp/dimens_material.xml
@@ -21,4 +21,8 @@
     <dimen name="screen_percentage_12">28.8dp</dimen>
     <dimen name="screen_percentage_15">36dp</dimen>
     <dimen name="screen_percentage_3646">87.5dp</dimen>
+
+    <!--  watch's indeterminate progress bar dimens based on the current screen size -->
+    <dimen name="progress_indeterminate_horizontal_min_width_watch">80dp</dimen>
+    <dimen name="progress_indeterminate_horizontal_min_height_watch">17dp</dimen>
 </resources>
diff --git a/core/res/res/values-watch/styles_device_defaults.xml b/core/res/res/values-watch/styles_device_defaults.xml
index fb7dbb0..eeb66e7 100644
--- a/core/res/res/values-watch/styles_device_defaults.xml
+++ b/core/res/res/values-watch/styles_device_defaults.xml
@@ -42,5 +42,8 @@
         <item name="indeterminateOnly">false</item>
         <!-- Use Wear Material3 ring shape as default determinate drawable -->
         <item name="progressDrawable">@drawable/progress_ring_watch</item>
+        <item name="indeterminateDrawable">@drawable/loader_horizontal_watch</item>
+        <item name="android:minWidth">@dimen/loader_horizontal_min_width_watch</item>
+        <item name="android:minHeight">@dimen/loader_horizontal_min_height_watch</item>
     </style>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1a311d57..2188469 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2643,6 +2643,15 @@
     <!-- MMS user agent prolfile url -->
     <string name="config_mms_user_agent_profile_url" translatable="false"></string>
 
+    <!-- The default list of possible CMF Names|Style|ColorSource. This array can be
+         overridden device-specific resources. A wildcard (fallback) must be supplied.
+         Name   - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required.
+         Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java
+         Color  - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) -->
+    <string-array name="theming_defaults">
+        <item>*|TONAL_SPOT|home_wallpaper</item>
+    </string-array>
+
     <!-- National Language Identifier codes for the following two config items.
          (from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1):
           0  - reserved
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e9d87e4..9acb242 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -580,9 +580,6 @@
     <dimen name="notification_text_size">14sp</dimen>
     <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) -->
     <dimen name="notification_title_text_size">14sp</dimen>
-    <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) -->
-    <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title -->
-    <dimen name="notification_2025_title_text_size">16sp</dimen>
     <!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) -->
     <dimen name="notification_big_title_text_size">16sp</dimen>
     <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) -->
diff --git a/core/res/res/values/dimens_watch.xml b/core/res/res/values/dimens_watch.xml
index 2aae987..1984591 100644
--- a/core/res/res/values/dimens_watch.xml
+++ b/core/res/res/values/dimens_watch.xml
@@ -61,4 +61,8 @@
     <dimen name="disabled_alpha_wear_material3">0.12</dimen>
     <!-- Alpha transparency applied to elements which are considered primary (e.g. primary text) -->
     <dimen name="primary_content_alpha_wear_material3">0.38</dimen>
+
+    <!--  watch's indeterminate progress bar dimens -->
+    <dimen name="loader_horizontal_min_width">68dp</dimen>
+    <dimen name="loader_horizontal_min_height">13dp</dimen>
 </resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index e3137e2..ac1e841 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -116,14 +116,14 @@
     <public name="alternateLauncherIcons"/>
     <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
     <public name="alternateLauncherLabels"/>
-    <!-- @hide Only for device overlay to use this. -->
-    <public name="pointerIconVectorFill"/>
-    <!-- @hide Only for device overlay to use this. -->
-    <public name="pointerIconVectorFillInverse"/>
-    <!-- @hide Only for device overlay to use this. -->
-    <public name="pointerIconVectorStroke"/>
-    <!-- @hide Only for device overlay to use this. -->
-    <public name="pointerIconVectorStrokeInverse"/>
+    <!-- @hide Wrongly added here. -->
+    <public name="removed_pointerIconVectorFill"/>
+    <!-- @hide Wrongly added here. -->
+    <public name="removed_pointerIconVectorFillInverse"/>
+    <!-- @hide Wrongly added here. -->
+    <public name="removed_pointerIconVectorStroke"/>
+    <!-- @hide Wrongly added here. -->
+    <public name="removed_pointerIconVectorStrokeInverse"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01b20000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index abbba9d..7a93ca1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1011,6 +1011,16 @@
     <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
     <string name="permgroupdesc_notifications">show notifications</string>
 
+    <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]-->
+    <string name="permgrouplab_xr_tracking">XR tracking data</string>
+    <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
+    <string name="permgroupdesc_xr_tracking">access XR data about you and the environment around you</string>
+
+    <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]-->
+    <string name="permgrouplab_xr_tracking_sensitive">sensitive XR tracking data</string>
+    <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
+    <string name="permgroupdesc_xr_tracking_sensitive">access sensitive tracking data, such as eye gaze</string>
+
     <!-- Title for the capability of an accessibility service to retrieve window content. -->
     <string name="capability_title_canRetrieveWindowContent">Retrieve window content</string>
     <!-- Description for the capability of an accessibility service to retrieve window content. -->
@@ -1875,6 +1885,45 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_mediaLocation">Allows the app to read locations from your media collection.</string>
 
+    <string name="permlab_eye_tracking_coarse">track your approximate eye gaze</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_eye_tracking_coarse">Allows the app to track your approximate eye gaze.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_eye_tracking_fine">track where you are looking</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_eye_tracking_fine">Allows the app to access precise eye gaze data.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_face_tracking">track your face</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_face_tracking">Allows the app to access face tracking data.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_hand_tracking">track your hands</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_hand_tracking">Allows the app to access hand tracking data.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_head_tracking">track your head</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_head_tracking">Allows the app to access head tracking data.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_scene_understanding_coarse">understand your immediate environment</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_scene_understanding_coarse">Allows the app to access tracking data about the environment directly around you.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_scene_understanding_fine">understand your immediate environment at high detail</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_scene_understanding_fine">Allows the app to access tracking data about the environment directly around you with very high detail.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_xr_tracking_in_background">access XR data while not in the foreground</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_xr_tracking_in_background">Allows the app to access XR data while not in the foreground.</string>
+
     <!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face). [CHAR LIMIT=30] -->
     <string name="biometric_app_setting_name">Use biometrics</string>
     <!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face) or their screen lock credential (i.e. PIN, pattern, or password). [CHAR LIMIT=70] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c62732d..06861b11 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -575,7 +575,6 @@
   <java-symbol type="dimen" name="notification_text_size" />
   <java-symbol type="dimen" name="notification_title_text_size" />
   <java-symbol type="dimen" name="notification_subtext_size" />
-  <java-symbol type="dimen" name="notification_2025_title_text_size" />
   <java-symbol type="dimen" name="notification_top_pad" />
   <java-symbol type="dimen" name="notification_top_pad_narrow" />
   <java-symbol type="dimen" name="notification_top_pad_large_text" />
@@ -1727,10 +1726,12 @@
   <java-symbol type="style" name="PointerIconVectorStyleFillBlue" />
   <java-symbol type="style" name="PointerIconVectorStyleFillPurple" />
   <java-symbol type="attr" name="pointerIconVectorFill" />
+  <java-symbol type="attr" name="pointerIconVectorFillInverse" />
   <java-symbol type="style" name="PointerIconVectorStyleStrokeWhite" />
   <java-symbol type="style" name="PointerIconVectorStyleStrokeBlack" />
   <java-symbol type="style" name="PointerIconVectorStyleStrokeNone" />
   <java-symbol type="attr" name="pointerIconVectorStroke" />
+  <java-symbol type="attr" name="pointerIconVectorStrokeInverse" />
   <java-symbol type="style" name="TextAppearance.DeviceDefault.Notification.Title" />
   <java-symbol type="style" name="TextAppearance.DeviceDefault.Notification.Info" />
 
@@ -5904,6 +5905,9 @@
   <java-symbol type="drawable" name="ic_notification_summarization" />
   <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" />
 
+  <!-- Device CMF Theming Settings -->
+  <java-symbol type="array" name="theming_defaults" />
+
   <!-- Advanced Protection Service USB feature -->
   <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" />
   <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index c06ad64..4c49ff8 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -10,8 +10,8 @@
 filegroup {
     name: "FrameworksCoreTests-aidl",
     srcs: [
-        "src/**/I*.aidl",
         "aidl/**/I*.aidl",
+        "src/**/I*.aidl",
     ],
     visibility: ["//visibility:private"],
 }
@@ -19,13 +19,13 @@
 filegroup {
     name: "FrameworksCoreTests-helpers",
     srcs: [
-        "DisabledTestApp/src/**/*.java",
-        "EnabledTestApp/src/**/*.java",
+        "AppThatCallsBinderMethods/src/**/*.kt",
+        "BinderDeathRecipientHelperApp/src/**/*.java",
         "BinderFrozenStateChangeCallbackTestApp/src/**/*.java",
         "BinderProxyCountingTestApp/src/**/*.java",
         "BinderProxyCountingTestService/src/**/*.java",
-        "BinderDeathRecipientHelperApp/src/**/*.java",
-        "AppThatCallsBinderMethods/src/**/*.kt",
+        "DisabledTestApp/src/**/*.java",
+        "EnabledTestApp/src/**/*.java",
     ],
     visibility: ["//visibility:private"],
 }
@@ -45,11 +45,11 @@
     defaults: ["FrameworksCoreTests-resources"],
 
     srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
+        ":FrameworksCoreTestDoubles-sources",
         ":FrameworksCoreTests-aidl",
         ":FrameworksCoreTests-helpers",
-        ":FrameworksCoreTestDoubles-sources",
+        "src/**/*.java",
+        "src/**/*.kt",
     ],
 
     aidl: {
@@ -65,74 +65,74 @@
         "-c fa",
     ],
     static_libs: [
-        "collector-device-lib-platform",
-        "frameworks-base-testutils",
-        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
-        "core-tests-support",
-        "cts-input-lib",
+        "TestParameterInjector",
         "android-common",
-        "frameworks-core-util-lib",
-        "mockwebserver",
-        "guava",
-        "guava-android-testlib",
         "android.app.usage.flags-aconfig-java",
+        "android.content.res.flags-aconfig-java",
+        "android.security.flags-aconfig-java",
         "android.view.accessibility.flags-aconfig-java",
         "androidx.core_core",
         "androidx.core_core-ktx",
         "androidx.test.core",
         "androidx.test.espresso.core",
         "androidx.test.ext.junit",
-        "androidx.test.runner",
         "androidx.test.rules",
-        "flag-junit",
-        "junit-params",
-        "kotlin-test",
-        "mockito-target-minus-junit4",
+        "androidx.test.runner",
         "androidx.test.uiautomator_uiautomator",
-        "platform-parametric-runner-lib",
-        "platform-test-annotations",
-        "platform-compat-test-rules",
-        "truth",
-        "print-test-util-lib",
-        "testng",
-        "servicestests-utils",
-        "device-time-shell-utils",
-        "testables",
+        "collector-device-lib-platform",
         "com.android.text.flags-aconfig-java",
+        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
+        "core-tests-support",
+        "cts-input-lib",
+        "device-time-shell-utils",
         "flag-junit",
-        "ravenwood-junit",
-        "perfetto_trace_java_protos",
+        "flag-junit",
         "flickerlib-parsers",
         "flickerlib-trace_processor_shell",
-        "mockito-target-extended-minus-junit4",
-        "TestParameterInjector",
-        "android.content.res.flags-aconfig-java",
-        "android.security.flags-aconfig-java",
+        "frameworks-base-testutils",
+        "frameworks-core-util-lib",
+        "guava",
+        "guava-android-testlib",
+        "junit-params",
+        "kotlin-test",
         "mockito-kotlin2",
+        "mockito-target-extended-minus-junit4",
+        "mockito-target-minus-junit4",
+        "mockwebserver",
+        "perfetto_trace_java_protos",
+        "platform-compat-test-rules",
+        "platform-parametric-runner-lib",
+        "platform-test-annotations",
+        "print-test-util-lib",
+        "ravenwood-junit",
+        "servicestests-utils",
+        "testables",
+        "testng",
+        "truth",
     ],
 
     libs: [
-        "android.test.runner.stubs",
-        "org.apache.http.legacy.stubs",
         "android.test.base.stubs",
         "android.test.mock.stubs",
-        "framework",
-        "ext",
-        "framework-res",
+        "android.test.runner.stubs",
         "android.view.flags-aconfig-java",
+        "ext",
+        "framework",
+        "framework-res",
+        "org.apache.http.legacy.stubs",
     ],
     jni_libs: [
+        "libAppOpsTest_jni",
         "libpowermanagertest_jni",
         "libviewRootImplTest_jni",
         "libworksourceparceltest_jni",
-        "libAppOpsTest_jni",
     ],
 
     sdk_version: "core_platform",
     test_suites: [
-        "device-tests",
-        "device-platinum-tests",
         "automotive-tests",
+        "device-platinum-tests",
+        "device-tests",
     ],
 
     certificate: "platform",
@@ -141,21 +141,21 @@
     java_resources: [":FrameworksCoreTests_unit_test_cert_der"],
 
     data: [
+        ":AppThatCallsBinderMethods",
+        ":AppThatUsesAppOps",
         ":BinderDeathRecipientHelperApp1",
         ":BinderDeathRecipientHelperApp2",
-        ":com.android.cts.helpers.aosp",
         ":BinderFrozenStateChangeCallbackTestApp",
         ":BinderProxyCountingTestApp",
         ":BinderProxyCountingTestService",
-        ":AppThatUsesAppOps",
-        ":AppThatCallsBinderMethods",
-        ":HelloWorldSdk1",
-        ":HelloWorldUsingSdk1AndSdk1",
-        ":HelloWorldUsingSdk1And2",
-        ":HelloWorldUsingSdkMalformedNegativeVersion",
         ":CtsStaticSharedLibConsumerApp1",
         ":CtsStaticSharedLibConsumerApp3",
         ":CtsStaticSharedLibProviderApp1",
+        ":HelloWorldSdk1",
+        ":HelloWorldUsingSdk1And2",
+        ":HelloWorldUsingSdk1AndSdk1",
+        ":HelloWorldUsingSdkMalformedNegativeVersion",
+        ":com.android.cts.helpers.aosp",
     ],
 }
 
@@ -170,8 +170,8 @@
     // FrameworksCoreTestsRavenwood references the .aapt.srcjar
     use_resource_processor: false,
     libs: [
-        "framework-res",
         "android.test.runner.stubs",
+        "framework-res",
         "org.apache.http.legacy.stubs",
     ],
     uses_libs: [
@@ -231,16 +231,16 @@
     static_libs: [
         "androidx.test.espresso.core",
         "androidx.test.ext.junit",
-        "androidx.test.runner",
         "androidx.test.rules",
+        "androidx.test.runner",
         "mockito-target-minus-junit4",
         "truth",
     ],
 
     libs: [
-        "android.test.runner.stubs.system",
         "android.test.base.stubs.system",
         "android.test.mock.stubs.system",
+        "android.test.runner.stubs.system",
         "framework",
         "framework-res",
     ],
@@ -249,43 +249,43 @@
 android_ravenwood_test {
     name: "FrameworksCoreTestsRavenwood",
     libs: [
-        "android.test.runner.stubs.system",
         "android.test.base.stubs.system",
+        "android.test.runner.stubs.system",
     ],
     static_libs: [
-        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
+        "androidx.annotation_annotation",
         "androidx.core_core",
         "androidx.core_core-ktx",
-        "androidx.annotation_annotation",
-        "androidx.test.rules",
         "androidx.test.ext.junit",
+        "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "compatibility-device-util-axt-ravenwood",
+        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
         "flag-junit",
-        "platform-test-annotations",
+        "flag-junit",
         "perfetto_trace_java_protos",
-        "flag-junit",
+        "platform-test-annotations",
         "testng",
     ],
     srcs: [
         "src/android/app/ActivityManagerTest.java",
+        "src/android/app/PropertyInvalidatedCacheTests.java",
         "src/android/colormodel/CamTest.java",
         "src/android/content/ContextTest.java",
+        "src/android/content/TestComponentCallbacks2.java",
         "src/android/content/pm/PackageManagerTest.java",
         "src/android/content/pm/UserInfoTest.java",
-        "src/android/app/PropertyInvalidatedCacheTests.java",
-        "src/android/database/CursorWindowTest.java",
-        "src/android/os/**/*.java",
         "src/android/content/res/*.java",
         "src/android/content/res/*.kt",
+        "src/android/database/CursorWindowTest.java",
+        "src/android/os/**/*.java",
         "src/android/telephony/PinResultTest.java",
         "src/android/util/**/*.java",
         "src/android/view/DisplayAdjustmentsTests.java",
-        "src/android/view/DisplayTest.java",
         "src/android/view/DisplayInfoTest.java",
+        "src/android/view/DisplayTest.java",
         "src/com/android/internal/logging/**/*.java",
         "src/com/android/internal/os/**/*.java",
-        "src/com/android/internal/util/**/*.java",
         "src/com/android/internal/power/EnergyConsumerStatsTest.java",
         "src/com/android/internal/ravenwood/**/*.java",
 
@@ -293,10 +293,12 @@
         // to avoid having a dependency to FrameworksCoreTests.
         // This way, when updating source files and running this test, we don't need to
         // rebuild the entire FrameworksCoreTests, which would be slow.
-        ":FrameworksCoreTests-resonly{.aapt.srcjar}",
+        "src/com/android/internal/util/**/*.java",
+
+        ":FrameworksCoreTestDoubles-sources",
         ":FrameworksCoreTests-aidl",
         ":FrameworksCoreTests-helpers",
-        ":FrameworksCoreTestDoubles-sources",
+        ":FrameworksCoreTests-resonly{.aapt.srcjar}",
     ],
     exclude_srcs: [
         "src/android/content/res/FontScaleConverterActivityTest.java",
@@ -320,8 +322,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
 }
@@ -331,12 +333,12 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "com.android.internal.inputmethod",
         "android.view.inputmethod",
+        "com.android.internal.inputmethod",
     ],
     exclude_annotations: ["androidx.test.filters.FlakyTest"],
 }
@@ -346,8 +348,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.ContextTest"],
 }
@@ -357,8 +359,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.app.KeyguardManagerTest"],
 }
@@ -368,8 +370,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.app.PropertyInvalidatedCacheTests"],
 }
@@ -379,12 +381,12 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "android.content.ContextTest",
         "android.content.ComponentCallbacksControllerTest",
+        "android.content.ContextTest",
         "android.content.ContextWrapperTest",
     ],
 }
@@ -394,8 +396,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.database.sqlite.SQLiteRawStatementTest"],
 }
@@ -405,8 +407,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.net"],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
@@ -417,8 +419,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.os.BatteryStatsTests"],
     exclude_annotations: ["com.android.internal.os.SkipPresubmit"],
@@ -429,8 +431,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.os.EnvironmentTest"],
 }
@@ -440,12 +442,12 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "com.android.internal.util.FastDataTest",
         "android.util.CharsetUtilsTest",
+        "com.android.internal.util.FastDataTest",
     ],
 }
 
@@ -454,12 +456,12 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "android.util.XmlTest",
         "android.util.BinaryXmlTest",
+        "android.util.XmlTest",
     ],
 }
 
@@ -468,8 +470,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.util.apk.SourceStampVerifierTest"],
 }
@@ -479,8 +481,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.view.textclassifier"],
     exclude_annotations: ["androidx.test.filters.FlakyTest"],
@@ -491,13 +493,13 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.app."],
     exclude_filters: [
-        "com.android.internal.app.WindowDecorActionBarTest",
         "com.android.internal.app.IntentForwarderActivityTest",
+        "com.android.internal.app.WindowDecorActionBarTest",
     ],
 }
 
@@ -506,8 +508,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.content."],
 }
@@ -517,8 +519,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.infra."],
 }
@@ -528,8 +530,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.jank"],
 }
@@ -539,16 +541,16 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "android.os.BinderProxyTest",
         "android.os.BinderDeathRecipientTest",
         "android.os.BinderFrozenStateChangeNotificationTest",
         "android.os.BinderProxyCountingTest",
-        "android.os.BinderUncaughtExceptionHandlerTest",
+        "android.os.BinderProxyTest",
         "android.os.BinderThreadPriorityTest",
+        "android.os.BinderUncaughtExceptionHandlerTest",
         "android.os.BinderWorkSourceTest",
         "android.os.ParcelNullabilityTest",
         "android.os.ParcelTest",
@@ -562,13 +564,13 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "com.android.internal.os.KernelCpuUidClusterTimeReaderTest",
-        "com.android.internal.os.KernelCpuUidBpfMapReaderTest",
         "com.android.internal.os.KernelCpuUidActiveTimeReaderTest",
+        "com.android.internal.os.KernelCpuUidBpfMapReaderTest",
+        "com.android.internal.os.KernelCpuUidClusterTimeReaderTest",
         "com.android.internal.os.KernelCpuUidFreqTimeReaderTest",
         "com.android.internal.os.KernelSingleUidTimeReaderTest",
     ],
@@ -579,8 +581,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.server.power.stats.BstatsCpuTimesValidationTest"],
 }
@@ -590,8 +592,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.security."],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
@@ -602,8 +604,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.util.LatencyTrackerTest"],
 }
@@ -613,8 +615,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.ContentCaptureOptionsTest"],
 }
@@ -624,8 +626,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.integrity."],
 }
@@ -635,8 +637,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.pm."],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
@@ -647,8 +649,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.pm."],
     include_annotations: ["android.platform.test.annotations.Postsubmit"],
@@ -659,14 +661,14 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.res."],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
     exclude_annotations: [
-        "androidx.test.filters.FlakyTest",
         "android.platform.test.annotations.Postsubmit",
+        "androidx.test.filters.FlakyTest",
         "org.junit.Ignore",
     ],
 }
@@ -676,8 +678,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.content.res."],
     include_annotations: ["android.platform.test.annotations.Postsubmit"],
@@ -688,17 +690,17 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
+        "android.service.controls",
+        "android.service.controls.actions",
+        "android.service.controls.templates",
         "android.service.euicc",
         "android.service.notification",
         "android.service.quicksettings",
         "android.service.settings.suggestions",
-        "android.service.controls.templates",
-        "android.service.controls.actions",
-        "android.service.controls",
     ],
     exclude_annotations: ["org.junit.Ignore"],
 }
@@ -708,8 +710,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.view.contentcapture"],
 }
@@ -719,8 +721,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.view.contentprotection"],
 }
@@ -730,8 +732,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.content."],
     include_annotations: ["android.platform.test.annotations.Presubmit"],
@@ -742,8 +744,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.graphics.drawable.IconTest"],
 }
@@ -753,13 +755,13 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "com.android.internal.accessibility",
         "android.accessibilityservice",
         "android.view.accessibility",
+        "com.android.internal.accessibility",
     ],
 }
 
@@ -768,8 +770,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.app.usage"],
 }
@@ -779,8 +781,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["com.android.internal.util.FastDataTest"],
 }
@@ -790,8 +792,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: ["android.hardware.input"],
 }
@@ -801,12 +803,12 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
-        "android.view.VerifiedMotionEventTest",
         "android.view.VerifiedKeyEventTest",
+        "android.view.VerifiedMotionEventTest",
     ],
 }
 
@@ -839,8 +841,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_filters: [
         "com.android.internal.jank.FrameTrackerTest",
@@ -854,8 +856,8 @@
     base: "FrameworksCoreTests",
     test_suites: [
         "automotive-tests",
-        "device-tests",
         "device-platinum-tests",
+        "device-tests",
     ],
     include_annotations: ["android.platform.test.annotations.PlatinumTest"],
 }
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 5765562..6fe3b6c 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -26,7 +26,6 @@
 import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Parcel;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 
@@ -112,7 +111,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void testLongInputsFromParcel() {
         // Create a rule with long fields, set directly via reflection so that we can confirm that
         // a rule with too-long fields that comes in via a parcel has its fields truncated directly.
@@ -169,7 +167,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void builderConstructor_nullInputs_throws() {
         assertThrows(NullPointerException.class,
                 () -> new AutomaticZenRule.Builder(null, Uri.parse("condition")));
@@ -178,7 +175,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void constructor_defaultTypeUnknown() {
         AutomaticZenRule rule = new AutomaticZenRule("name", new ComponentName("pkg", "cps"), null,
                 Uri.parse("conditionId"), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY,
@@ -188,7 +184,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void builder_defaultsAreSensible() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("name",
                 Uri.parse("conditionId")).build();
@@ -200,7 +195,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void validate_builderWithValidType_succeeds() throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
                 .setType(AutomaticZenRule.TYPE_BEDTIME)
@@ -209,14 +203,12 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void validate_builderWithoutType_succeeds() throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
         rule.validate(); // No exception.
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void validate_constructorWithoutType_succeeds() throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule("rule", new ComponentName("pkg", "cps"),
                 new ComponentName("pkg", "activity"), Uri.parse("condition"), null,
@@ -225,7 +217,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void validate_invalidType_throws() throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
 
@@ -238,7 +229,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void setType_invalidType_throws() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
 
@@ -246,7 +236,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void setTypeBuilder_invalidType_throws() {
         AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("rule", Uri.parse("uri"));
 
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index d816039..250b9ce 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -521,7 +521,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @EnableFlags(Flags.FLAG_MODES_UI)
     public void areAutomaticZenRulesUserManaged_handheld_isTrue() {
         PackageManager pm = mock(PackageManager.class);
         when(pm.hasSystemFeature(any())).thenReturn(false);
@@ -531,7 +531,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @EnableFlags(Flags.FLAG_MODES_UI)
     public void areAutomaticZenRulesUserManaged_auto_isFalse() {
         PackageManager pm = mock(PackageManager.class);
         when(pm.hasSystemFeature(eq(PackageManager.FEATURE_AUTOMOTIVE))).thenReturn(true);
@@ -541,7 +541,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @EnableFlags(Flags.FLAG_MODES_UI)
     public void areAutomaticZenRulesUserManaged_tv_isFalse() {
         PackageManager pm = mock(PackageManager.class);
         when(pm.hasSystemFeature(eq(PackageManager.FEATURE_LEANBACK))).thenReturn(true);
@@ -551,7 +551,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @EnableFlags(Flags.FLAG_MODES_UI)
     public void areAutomaticZenRulesUserManaged_watch_isFalse() {
         PackageManager pm = mock(PackageManager.class);
         when(pm.hasSystemFeature(eq(PackageManager.FEATURE_WATCH))).thenReturn(true);
diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
index 7b41217..ab2e77e 100644
--- a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -23,6 +23,8 @@
 
 import static junit.framework.Assert.fail;
 
+import static org.junit.Assert.assertThrows;
+
 import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
@@ -32,6 +34,8 @@
 
 import com.android.server.backup.Flags;
 
+import com.google.common.truth.Expect;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -42,6 +46,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 @Presubmit
@@ -64,6 +69,9 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     @Before
     public void setUp() throws Exception {
         mHashDigest = MessageDigest.getInstance("SHA-256");
@@ -366,6 +374,32 @@
         assertThat(mLogger.getLoggingResults()).isEmpty();
     }
 
+    @Test
+    public void testDataTypeResultToString_nullArgs() {
+        assertThrows(NullPointerException.class, () -> BackupRestoreEventLogger.toString(null));
+    }
+
+    @Test
+    public void testDataTypeResultToString_typeOnly() {
+        DataTypeResult result = new DataTypeResult("The Type is Bond, James Bond!");
+
+        expect.withMessage("toString()")
+                .that(BackupRestoreEventLogger.toString(result)).isEqualTo(
+                        "type=The Type is Bond, James Bond!, successCount=0, failCount=0");
+    }
+
+    @Test
+    public void testDataTypeResultToString_allFields() {
+        DataTypeResult result = DataTypeResultTest.createDataTypeResult(
+                "The Type is Bond, James Bond!", /* successCount= */ 42, /* failCount= */ 108,
+                Map.of("D'OH!", 666, "", 0), new byte[] { 4, 8, 15, 16, 23, 42 });
+
+        expect.withMessage("toString()")
+                .that(BackupRestoreEventLogger.toString(result)).isEqualTo(
+                        "type=The Type is Bond, James Bond!, successCount=42, failCount=108, "
+                        + "errors={=0, D'OH!=666}, metadataHash=[4, 8, 15, 16, 23, 42]");
+    }
+
     private static DataTypeResult getResultForDataType(
             BackupRestoreEventLogger logger, String dataType) {
         Optional<DataTypeResult> result = getResultForDataTypeIfPresent(logger, dataType);
diff --git a/core/tests/coretests/src/android/app/backup/DataTypeResultTest.java b/core/tests/coretests/src/android/app/backup/DataTypeResultTest.java
new file mode 100644
index 0000000..cf9e9c6
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/DataTypeResultTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2025 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 android.app.backup;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Map;
+
+public final class DataTypeResultTest {
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    @Test
+    public void testGetters_defaultConstructorFields() {
+        var result = new DataTypeResult("The Type is Bond, James Bond!");
+
+        expect.withMessage("getDataType()").that(result.getDataType())
+                .isEqualTo("The Type is Bond, James Bond!");
+        expect.withMessage("getSuccessCount()").that(result.getSuccessCount()).isEqualTo(0);
+        expect.withMessage("getFailCount()").that(result.getFailCount()).isEqualTo(0);
+        expect.withMessage("getErrorsCount()").that(result.getErrors()).isEmpty();
+        expect.withMessage("getMetadataHash()").that(result.getMetadataHash()).isNull();
+        expect.withMessage("describeContents()").that(result.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetters_allFields() {
+        DataTypeResult result = createDataTypeResult("The Type is Bond, James Bond!",
+                /* successCount= */ 42, /* failCount= */ 108, Map.of("D'OH!", 666),
+                new byte[] { 4, 8, 15, 16, 23, 42 });
+
+        expect.withMessage("getDataType()").that(result.getDataType())
+                .isEqualTo("The Type is Bond, James Bond!");
+        expect.withMessage("getSuccessCount()").that(result.getSuccessCount()).isEqualTo(42);
+        expect.withMessage("getFailCount()").that(result.getFailCount()).isEqualTo(108);
+        expect.withMessage("getErrorsCount()").that(result.getErrors()).containsExactly("D'OH!",
+                666);
+        expect.withMessage("getMetadataHash()").that(result.getMetadataHash()).asList()
+                .containsExactly((byte) 4, (byte) 8, (byte) 15, (byte) 16, (byte) 23, (byte) 42)
+                .inOrder();
+        expect.withMessage("describeContents()").that(result.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testParcelMethods() {
+        DataTypeResult original = createDataTypeResult("The Type is Bond, James Bond!",
+                /* successCount= */ 42, /* failCount= */ 108, Map.of("D'OH!", 666),
+                new byte[] { 4, 8, 15, 16, 23, 42 });
+        Parcel parcel = Parcel.obtain();
+        try {
+            original.writeToParcel(parcel, /* flags= */ 0);
+
+            parcel.setDataPosition(0);
+            var clone = DataTypeResult.CREATOR.createFromParcel(parcel);
+            assertWithMessage("createFromParcel()").that(clone).isNotNull();
+
+            expect.withMessage("getDataType()").that(clone.getDataType())
+                    .isEqualTo(original.getDataType());
+            expect.withMessage("getSuccessCount()").that(clone.getSuccessCount())
+                    .isEqualTo(original.getSuccessCount());
+            expect.withMessage("getFailCount()").that(clone.getFailCount())
+                    .isEqualTo(original.getFailCount());
+            expect.withMessage("getErrorsCount()").that(clone.getErrors())
+                    .containsExactlyEntriesIn(original.getErrors()).inOrder();
+            expect.withMessage("getMetadataHash()").that(clone.getMetadataHash())
+                    .isEqualTo(original.getMetadataHash());
+            expect.withMessage("describeContents()").that(clone.describeContents()).isEqualTo(0);
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+    static DataTypeResult createDataTypeResult(String dataType, int successCount, int failCount,
+            Map<String, Integer> errors, byte... metadataHash) {
+        Parcel parcel = Parcel.obtain();
+        try {
+            parcel.writeString(dataType);
+            parcel.writeInt(successCount);
+            parcel.writeInt(failCount);
+            Bundle errorsBundle = new Bundle();
+            errors.entrySet()
+                    .forEach(entry -> errorsBundle.putInt(entry.getKey(), entry.getValue()));
+            parcel.writeBundle(errorsBundle);
+            parcel.writeByteArray(metadataHash);
+
+            parcel.setDataPosition(0);
+            var result = DataTypeResult.CREATOR.createFromParcel(parcel);
+            assertWithMessage("createFromParcel()").that(result).isNotNull();
+            return result;
+        } finally {
+            parcel.recycle();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/content/ContextTest.java b/core/tests/coretests/src/android/content/ContextTest.java
index a02af78..2505500 100644
--- a/core/tests/coretests/src/android/content/ContextTest.java
+++ b/core/tests/coretests/src/android/content/ContextTest.java
@@ -23,6 +23,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -35,17 +36,24 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.media.ImageReader;
+import android.os.Looper;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
+import android.window.WindowTokenClient;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.window.flags.Flags;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -61,6 +69,9 @@
     @Rule
     public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Test
     public void testInstrumentationContext() {
         // Confirm that we have a valid Context
@@ -280,4 +291,44 @@
         return appContext.createDisplayContext(display)
                 .createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
     }
+
+    @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
+    @DisableFlags(Flags.FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS)
+    public void testSysUiContextRegisterComponentCallbacks_disableFlag() {
+        Looper.prepare();
+
+        // Use createSystemActivityThreadForTesting to initialize
+        // systemUiContext#getApplicationContext.
+        final Context systemUiContext = ActivityThread.createSystemActivityThreadForTesting()
+                .getSystemUiContext();
+        final TestComponentCallbacks2 callbacks = new TestComponentCallbacks2();
+        systemUiContext.registerComponentCallbacks(callbacks);
+
+        final WindowTokenClient windowTokenClient =
+                (WindowTokenClient) systemUiContext.getWindowContextToken();
+        windowTokenClient.onConfigurationChanged(Configuration.EMPTY, DEFAULT_DISPLAY);
+
+        assertWithMessage("ComponentCallbacks should delegate to the app Context "
+                + "if the flag is disabled.").that(callbacks.mConfiguration).isNull();
+    }
+
+    @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
+    @EnableFlags(Flags.FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS)
+    public void testSysUiContextRegisterComponentCallbacks_enableFlag() {
+        final Context systemUiContext = ActivityThread.currentActivityThread()
+                .createSystemUiContextForTesting(DEFAULT_DISPLAY);
+        final TestComponentCallbacks2 callbacks = new TestComponentCallbacks2();
+        final Configuration config = Configuration.EMPTY;
+
+        systemUiContext.registerComponentCallbacks(callbacks);
+
+        final WindowTokenClient windowTokenClient =
+                (WindowTokenClient) systemUiContext.getWindowContextToken();
+        windowTokenClient.onConfigurationChanged(config, DEFAULT_DISPLAY);
+
+        assertWithMessage("ComponentCallbacks should delegate to SystemUiContext "
+                + "if the flag is enabled.").that(callbacks.mConfiguration).isEqualTo(config);
+    }
 }
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index dc2f0a6..9383807 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -33,6 +33,7 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -348,6 +349,26 @@
                                 DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS));
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT)
+    public void test_mapPrivateEventCommittedStateChanged_flagEnabled() {
+        // Test public flags mapping
+        assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED,
+                mDisplayManagerGlobal
+                        .mapFiltersToInternalEventFlag(0,
+                                DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED));
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT)
+    public void test_mapPrivateEventCommittedStateChanged_flagDisabled() {
+        // Test public flags mapping
+        assertEquals(0,
+                mDisplayManagerGlobal
+                        .mapFiltersToInternalEventFlag(0,
+                                DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED));
+    }
+
     private void waitForHandler() {
         mHandler.runWithScissors(() -> {
         }, 0);
diff --git a/core/tests/coretests/src/android/service/notification/ConditionTest.java b/core/tests/coretests/src/android/service/notification/ConditionTest.java
index e94273e..65c108a 100644
--- a/core/tests/coretests/src/android/service/notification/ConditionTest.java
+++ b/core/tests/coretests/src/android/service/notification/ConditionTest.java
@@ -23,10 +23,8 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.app.Flags;
 import android.net.Uri;
 import android.os.Parcel;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -113,7 +111,6 @@
 
     @Test
     public void testLongFields_inConstructors() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         String longString = Strings.repeat("A", 65536);
         Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530));
 
@@ -136,7 +133,6 @@
 
     @Test
     public void testLongFields_viaParcel() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         // Set fields via reflection to force them to be long, then parcel and unparcel to make sure
         // it gets truncated upon unparcelling.
         Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
@@ -170,8 +166,6 @@
 
     @Test
     public void testEquals() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         Condition cond1 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
                 Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
         Condition cond2 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
@@ -186,8 +180,6 @@
 
     @Test
     public void testParcelConstructor() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
                 Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
 
@@ -200,28 +192,24 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void constructor_unspecifiedSource_succeeds() {
         new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE);
         // No exception.
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void constructor_validSource_succeeds() {
         new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE, Condition.SOURCE_CONTEXT);
         // No exception.
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void constructor_invalidSource_throws() {
         assertThrows(IllegalArgumentException.class,
                 () -> new Condition(Uri.parse("uri"), "Summary", Condition.STATE_TRUE, 1000));
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void constructor_parcelWithInvalidSource_throws() {
         Condition original = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE,
                 Condition.SOURCE_SCHEDULE);
@@ -237,7 +225,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void validate_invalidSource_throws() throws Exception {
         Condition condition = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE,
                 Condition.SOURCE_SCHEDULE);
diff --git a/data/keyboards/Vendor_0957_Product_0001.kl b/data/keyboards/Vendor_0957_Product_0001.kl
index 87cb942..0241f36 100644
--- a/data/keyboards/Vendor_0957_Product_0001.kl
+++ b/data/keyboards/Vendor_0957_Product_0001.kl
@@ -72,6 +72,8 @@
 key usage 0x000c008D    GUIDE
 key usage 0x000c0089    TV
 
+key usage 0x000c0187    FEATURED_APP_1    WAKE #FreeTv
+
 key usage 0x000c009C    CHANNEL_UP
 key usage 0x000c009D    CHANNEL_DOWN
 
diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java
index 7d236d2..3b8f466 100644
--- a/graphics/java/android/graphics/FrameInfo.java
+++ b/graphics/java/android/graphics/FrameInfo.java
@@ -93,10 +93,12 @@
     // Interval between two consecutive frames
     public static final int FRAME_INTERVAL = 11;
 
+    // Workload target deadline for a frame
+    public static final int WORKLOAD_TARGET = 12;
+
     // Must be the last one
     // This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h
-    // In calculating size, + 1 for Flags, and + 1 for WorkloadTarget from FrameInfo.h
-    private static final int FRAME_INFO_SIZE = FRAME_INTERVAL + 2;
+    private static final int FRAME_INFO_SIZE = WORKLOAD_TARGET + 1;
 
     /** checkstyle */
     public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId,
@@ -108,6 +110,7 @@
         frameInfo[FRAME_DEADLINE] = frameDeadline;
         frameInfo[FRAME_START_TIME] = frameStartTime;
         frameInfo[FRAME_INTERVAL] = frameInterval;
+        frameInfo[WORKLOAD_TARGET] = frameDeadline - intendedVsync;
     }
 
     /** checkstyle */
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 940cd93..65854dd 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -1467,6 +1467,18 @@
     public static native void preload();
 
     /**
+     * Initialize the Buffer Allocator singleton
+     *
+     * This takes 10-20ms on low-resourced devices, so doing it on-demand when an app
+     * tries to render its first frame causes drawFrames to be blocked for buffer
+     * allocation due to just initializing the allocator.
+     *
+     * Should only be called when a buffer is expected to be used.
+     * @hide
+     */
+    public static native void preInitBufferAllocator();
+
+    /**
      * @hide
      */
     protected static native boolean isWebViewOverlaysEnabled();
diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java
index 3543e99..db2376e 100644
--- a/graphics/java/android/graphics/RuntimeShader.java
+++ b/graphics/java/android/graphics/RuntimeShader.java
@@ -20,6 +20,7 @@
 import android.annotation.ColorLong;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.util.ArrayMap;
 import android.view.Window;
 
@@ -76,6 +77,7 @@
  * Additionally, if the shader is invoked by another using {@link #setInputShader(String, Shader)},
  * then that parent shader may modify the input coordinates arbitrarily.</p>
  *
+ * <a id="agsl-and-color-spaces"/>
  * <h3>AGSL and Color Spaces</h3>
  * <p>Android Graphics and by extension {@link RuntimeShader} are color managed.  The working
  * {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which
@@ -267,6 +269,8 @@
     private ArrayMap<String, ColorFilter> mColorFilterUniforms = new ArrayMap<>();
     private ArrayMap<String, RuntimeXfermode> mXfermodeUniforms = new ArrayMap<>();
 
+    private ColorSpace mWorkingColorSpace = null;
+
 
     /**
      * Creates a new RuntimeShader.
@@ -286,6 +290,35 @@
     }
 
     /**
+     * Sets the working color space for this shader. That is, the shader will be evaluated
+     * in the given colorspace before being converted to the output destination's colorspace.
+     *
+     * <p>By default the RuntimeShader is evaluated in the context of the
+     * <a href="#agsl-and-color-spaces">destination colorspace</a>. By calling this method
+     * that can be overridden to force the shader to be evaluated in the given colorspace first
+     * before then being color converted to the destination colorspace.</p>
+     *
+     * @param colorSpace The ColorSpace to evaluate in. Must be an {@link ColorSpace#getModel() RGB}
+     *                   ColorSpace. Passing null restores default behavior of working in the
+     *                   destination colorspace.
+     * @throws IllegalArgumentException If the colorspace is not RGB
+     */
+    @FlaggedApi(Flags.FLAG_SHADER_COLOR_SPACE)
+    public void setWorkingColorSpace(@Nullable ColorSpace colorSpace) {
+        if (colorSpace != null && colorSpace.getModel() != ColorSpace.Model.RGB) {
+            throw new IllegalArgumentException("ColorSpace must be RGB, given " + colorSpace);
+        }
+        if (mWorkingColorSpace != colorSpace) {
+            mWorkingColorSpace = colorSpace;
+            if (mWorkingColorSpace != null) {
+                // Just to enforce this can be resolved instead of erroring out later
+                mWorkingColorSpace.getNativeInstance();
+            }
+            discardNativeInstance();
+        }
+    }
+
+    /**
      * Sets the uniform color value corresponding to this shader.  If the shader does not have a
      * uniform with that name or if the uniform is declared with a type other than vec3 or vec4 and
      * corresponding layout(color) annotation then an IllegalArgumentException is thrown.
@@ -578,7 +611,8 @@
     /** @hide */
     @Override
     protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) {
-        return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix);
+        return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix,
+                mWorkingColorSpace != null ? mWorkingColorSpace.getNativeInstance() : 0);
     }
 
     /** @hide */
@@ -589,6 +623,8 @@
     private static native long nativeGetFinalizer();
     private static native long nativeCreateBuilder(String agsl);
     private static native long nativeCreateShader(long shaderBuilder, long matrix);
+    private static native long nativeCreateShader(long shaderBuilder, long matrix,
+            long colorSpacePtr);
     private static native void nativeUpdateUniforms(
             long shaderBuilder, String uniformName, float[] uniforms, boolean isColor);
     private static native void nativeUpdateUniforms(
diff --git a/keystore/java/android/security/GateKeeper.java b/keystore/java/android/security/GateKeeper.java
index 464714f..c2792e1f 100644
--- a/keystore/java/android/security/GateKeeper.java
+++ b/keystore/java/android/security/GateKeeper.java
@@ -28,7 +28,7 @@
  *
  * @hide
  */
-public abstract class GateKeeper {
+public final class GateKeeper {
 
     public static final long INVALID_SECURE_USER_ID = 0;
 
diff --git a/keystore/java/android/security/keystore/ArrayUtils.java b/keystore/java/android/security/keystore/ArrayUtils.java
index f22b604..6472ca9 100644
--- a/keystore/java/android/security/keystore/ArrayUtils.java
+++ b/keystore/java/android/security/keystore/ArrayUtils.java
@@ -23,7 +23,7 @@
 /**
  * @hide
  */
-public abstract class ArrayUtils {
+public final class ArrayUtils {
     private ArrayUtils() {}
 
     public static String[] nullToEmpty(String[] array) {
diff --git a/keystore/java/android/security/keystore/Utils.java b/keystore/java/android/security/keystore/Utils.java
index e58b1cc..c38ce8e 100644
--- a/keystore/java/android/security/keystore/Utils.java
+++ b/keystore/java/android/security/keystore/Utils.java
@@ -23,7 +23,7 @@
  *
  * @hide
  */
-abstract class Utils {
+public final class Utils {
     private Utils() {}
 
     static Date cloneIfNotNull(Date value) {
diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
index 1394bd4..9d306ce 100644
--- a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
+++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
@@ -38,7 +38,9 @@
 /**
  * @hide
  */
-public abstract class KeyStore2ParameterUtils {
+public final class KeyStore2ParameterUtils {
+
+    private KeyStore2ParameterUtils() {}
 
     /**
      * This function constructs a {@link KeyParameter} expressing a boolean value.
diff --git a/keystore/java/android/security/keystore2/KeymasterUtils.java b/keystore/java/android/security/keystore2/KeymasterUtils.java
index 614e368..02f3f57 100644
--- a/keystore/java/android/security/keystore2/KeymasterUtils.java
+++ b/keystore/java/android/security/keystore2/KeymasterUtils.java
@@ -16,13 +16,10 @@
 
 package android.security.keystore2;
 
-import android.security.keymaster.KeymasterArguments;
 import android.security.keymaster.KeymasterDefs;
-import android.security.keystore.KeyProperties;
 
 import java.security.AlgorithmParameters;
 import java.security.NoSuchAlgorithmException;
-import java.security.ProviderException;
 import java.security.spec.ECGenParameterSpec;
 import java.security.spec.ECParameterSpec;
 import java.security.spec.InvalidParameterSpecException;
@@ -30,7 +27,7 @@
 /**
  * @hide
  */
-public abstract class KeymasterUtils {
+public final class KeymasterUtils {
 
     private KeymasterUtils() {}
 
@@ -86,47 +83,6 @@
         }
     }
 
-    /**
-     * Adds {@code KM_TAG_MIN_MAC_LENGTH} tag, if necessary, to the keymaster arguments for
-     * generating or importing a key. This tag may only be needed for symmetric keys (e.g., HMAC,
-     * AES-GCM).
-     */
-    public static void addMinMacLengthAuthorizationIfNecessary(KeymasterArguments args,
-            int keymasterAlgorithm,
-            int[] keymasterBlockModes,
-            int[] keymasterDigests) {
-        switch (keymasterAlgorithm) {
-            case KeymasterDefs.KM_ALGORITHM_AES:
-                if (com.android.internal.util.ArrayUtils.contains(
-                        keymasterBlockModes, KeymasterDefs.KM_MODE_GCM)) {
-                    // AES GCM key needs the minimum length of AEAD tag specified.
-                    args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH,
-                            AndroidKeyStoreAuthenticatedAESCipherSpi.GCM
-                                    .MIN_SUPPORTED_TAG_LENGTH_BITS);
-                }
-                break;
-            case KeymasterDefs.KM_ALGORITHM_HMAC:
-                // HMAC key needs the minimum length of MAC set to the output size of the associated
-                // digest. This is because we do not offer a way to generate shorter MACs and
-                // don't offer a way to verify MACs (other than by generating them).
-                if (keymasterDigests.length != 1) {
-                    throw new ProviderException(
-                            "Unsupported number of authorized digests for HMAC key: "
-                                    + keymasterDigests.length
-                                    + ". Exactly one digest must be authorized");
-                }
-                int keymasterDigest = keymasterDigests[0];
-                int digestOutputSizeBits = getDigestOutputSizeBits(keymasterDigest);
-                if (digestOutputSizeBits == -1) {
-                    throw new ProviderException(
-                            "HMAC key authorized for unsupported digest: "
-                                    + KeyProperties.Digest.fromKeymaster(keymasterDigest));
-                }
-                args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, digestOutputSizeBits);
-                break;
-        }
-    }
-
     static String getEcCurveFromKeymaster(int ecCurve) {
         switch (ecCurve) {
             case android.hardware.security.keymint.EcCurve.P_224:
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index ab2f3ef..68970e6 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -3,5 +3,5 @@
 pragyabajoria@google.com
 
 # Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com, peanutbutter@google.com, jeremysim@google.com
 per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png
new file mode 100644
index 0000000..1519874
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png
new file mode 100644
index 0000000..99673f6
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png
new file mode 100644
index 0000000..ba4ebab7
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png
new file mode 100644
index 0000000..b3ff644
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png
new file mode 100644
index 0000000..534e320
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png
new file mode 100644
index 0000000..67c9f49
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png
new file mode 100644
index 0000000..a0fb490
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png
new file mode 100644
index 0000000..27b35d4
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png
new file mode 100644
index 0000000..11528a0
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png
new file mode 100644
index 0000000..ef99377
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png
new file mode 100644
index 0000000..f0cf08b
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png
new file mode 100644
index 0000000..bbaafb3
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png
new file mode 100644
index 0000000..38ebf3f
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png
new file mode 100644
index 0000000..2e4fd51
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png
new file mode 100644
index 0000000..a1ba9fb
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png
new file mode 100644
index 0000000..51bb15e
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png
new file mode 100644
index 0000000..b643e2a
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png
new file mode 100644
index 0000000..e6eeab7
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt
new file mode 100644
index 0000000..24f43d3
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2025 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.shared.bubbles
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.annotation.ColorInt
+import androidx.core.graphics.blue
+import androidx.core.graphics.green
+import androidx.core.graphics.red
+import androidx.core.graphics.toColorInt
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
+import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.ViewScreenshotTestRule.Mode
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+@RunWith(ParameterizedAndroidJunit4::class)
+class DragZoneFactoryScreenshotTest(private val param: Param) {
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs(): List<Param> {
+            val params = mutableListOf<Param>()
+            val draggedObjects =
+                listOf(
+                    DraggedObject.Bubble(BubbleBarLocation.LEFT),
+                    DraggedObject.BubbleBar(BubbleBarLocation.LEFT),
+                    DraggedObject.ExpandedView(BubbleBarLocation.LEFT),
+                )
+            DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = false).forEach { tablet
+                ->
+                draggedObjects.forEach { draggedObject ->
+                    params.add(Param(tablet, draggedObject, SplitScreenMode.NONE))
+                }
+            }
+            DeviceEmulationSpec.forDisplays(Displays.FoldableInner, isDarkTheme = false).forEach {
+                foldable ->
+                draggedObjects.forEach { draggedObject ->
+                    params.add(Param(foldable, draggedObject, SplitScreenMode.NONE))
+                    val isBubble = draggedObject is DraggedObject.Bubble
+                    val isExpandedView = draggedObject is DraggedObject.ExpandedView
+                    val addMoreSplitModes = isBubble || (isExpandedView && foldable.isLandscape)
+                    if (addMoreSplitModes) {
+                        params.add(Param(foldable, draggedObject, SplitScreenMode.SPLIT_10_90))
+                        params.add(Param(foldable, draggedObject, SplitScreenMode.SPLIT_90_10))
+                    }
+                }
+            }
+            return params
+        }
+    }
+
+    class Param(
+        val emulationSpec: DeviceEmulationSpec,
+        val draggedObject: DraggedObject,
+        val splitScreenMode: SplitScreenMode
+    ) {
+        private val draggedObjectName =
+            when (draggedObject) {
+                is DraggedObject.Bubble -> "bubble"
+                is DraggedObject.BubbleBar -> "bubbleBar"
+                is DraggedObject.ExpandedView -> "expandedView"
+            }
+
+        private val splitScreenModeName =
+            when (splitScreenMode) {
+                SplitScreenMode.NONE -> ""
+                SplitScreenMode.SPLIT_50_50 -> "_split_50_50"
+                SplitScreenMode.SPLIT_10_90 -> "_split_10_90"
+                SplitScreenMode.SPLIT_90_10 -> "_split_90_10"
+            }
+
+        val testName = "$draggedObjectName$splitScreenModeName"
+
+        override fun toString() = "${emulationSpec}_$testName"
+    }
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            param.emulationSpec,
+            WMShellGoldenPathManager(getEmulatedDevicePathConfig(param.emulationSpec))
+        )
+
+    private val context = getApplicationContext<Context>()
+
+    @Test
+    fun dragZones() {
+        screenshotRule.screenshotTest("dragZones_${param.testName}", mode = Mode.MatchSize) {
+            activity ->
+            activity.actionBar?.hide()
+            val dragZoneFactory = createDragZoneFactory()
+            val dragZones = dragZoneFactory.createSortedDragZones(param.draggedObject)
+            val container = FrameLayout(context)
+            dragZones.forEach { zone -> container.addZoneView(zone) }
+            container
+        }
+    }
+
+    private fun createDragZoneFactory(): DragZoneFactory {
+        val deviceConfig =
+            DeviceConfig.create(context, context.getSystemService(WindowManager::class.java)!!)
+        val splitScreenModeChecker = SplitScreenModeChecker { param.splitScreenMode }
+        val desktopWindowModeChecker = DesktopWindowModeChecker { true }
+        return DragZoneFactory(
+            context,
+            deviceConfig,
+            splitScreenModeChecker,
+            desktopWindowModeChecker
+        )
+    }
+
+    private fun FrameLayout.addZoneView(zone: DragZone) {
+        val view = View(context)
+        this.addView(view, 0)
+        view.layoutParams = FrameLayout.LayoutParams(zone.bounds.width(), zone.bounds.height())
+        view.background = createZoneDrawable(zone.color)
+        view.x = zone.bounds.left.toFloat()
+        view.y = zone.bounds.top.toFloat()
+    }
+
+    private fun createZoneDrawable(@ColorInt color: Int): Drawable {
+        val shape = GradientDrawable()
+        shape.shape = GradientDrawable.RECTANGLE
+        shape.setColor(Color.argb(128, color.red, color.green, color.blue))
+        shape.setStroke(2, color)
+        return shape
+    }
+
+    private val DragZone.color: Int
+        @ColorInt
+        get() =
+            when (this) {
+                is DragZone.Bubble -> "#3F5C8B".toColorInt()
+                is DragZone.Dismiss -> "#8B3F3F".toColorInt()
+                is DragZone.Split -> "#89B675".toColorInt()
+                is DragZone.FullScreen -> "#4ED075".toColorInt()
+                is DragZone.DesktopWindow -> "#EC928E".toColorInt()
+            }
+}
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 8d7e5fd..d50a14c 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -18,23 +18,28 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/maximize_menu"
     android:layout_width="wrap_content"
-    android:layout_height="@dimen/desktop_mode_maximize_menu_height"
+    android:layout_height="wrap_content"
     android:background="@drawable/desktop_mode_maximize_menu_background"
     android:elevation="1dp">
 
     <LinearLayout
         android:id="@+id/container"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/desktop_mode_maximize_menu_height"
+        android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:padding="16dp"
+        android:paddingHorizontal="12dp"
+        android:paddingVertical="16dp"
+        android:measureWithLargestChild="true"
         android:gravity="center">
 
         <LinearLayout
             android:id="@+id/maximize_menu_immersive_toggle_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:orientation="vertical">
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:layout_marginStart="4dp"
+            android:layout_marginEnd="4dp">
 
             <Button
                 android:layout_width="94dp"
@@ -44,21 +49,22 @@
                 android:stateListAnimator="@null"
                 android:importantForAccessibility="yes"
                 android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text"
-                android:layout_marginEnd="8dp"
                 android:layout_marginBottom="4dp"
                 android:alpha="0"/>
 
             <TextView
                 android:id="@+id/maximize_menu_immersive_toggle_button_text"
-                android:layout_width="94dp"
-                android:layout_height="18dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:textSize="11sp"
-                android:layout_marginBottom="76dp"
+                android:lineHeight="16sp"
                 android:gravity="center"
                 android:fontFamily="google-sans-text"
+                android:textFontWeight="500"
                 android:importantForAccessibility="no"
                 android:text="@string/desktop_mode_maximize_menu_immersive_button_text"
                 android:textColor="@androidprv:color/materialColorOnSurface"
+                android:singleLine="true"
                 android:alpha="0"/>
         </LinearLayout>
 
@@ -66,7 +72,11 @@
             android:id="@+id/maximize_menu_size_toggle_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:orientation="vertical">
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:gravity="center_horizontal"
+            android:layout_marginStart="4dp"
+            android:layout_marginEnd="4dp">
 
             <Button
                 android:layout_width="94dp"
@@ -81,15 +91,17 @@
 
             <TextView
                 android:id="@+id/maximize_menu_size_toggle_button_text"
-                android:layout_width="94dp"
-                android:layout_height="18dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:textSize="11sp"
-                android:layout_marginBottom="76dp"
+                android:lineHeight="16sp"
                 android:gravity="center"
                 android:fontFamily="google-sans-text"
+                android:textFontWeight="500"
                 android:importantForAccessibility="no"
                 android:text="@string/desktop_mode_maximize_menu_maximize_text"
                 android:textColor="@androidprv:color/materialColorOnSurface"
+                android:singleLine="true"
                 android:alpha="0"/>
         </LinearLayout>
 
@@ -97,7 +109,11 @@
             android:id="@+id/maximize_menu_snap_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:orientation="vertical">
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:gravity="center_horizontal"
+            android:layout_marginStart="4dp"
+            android:layout_marginEnd="4dp">
             <LinearLayout
                 android:id="@+id/maximize_menu_snap_menu_layout"
                 android:layout_width="wrap_content"
@@ -106,7 +122,6 @@
                 android:padding="4dp"
                 android:background="@drawable/desktop_mode_maximize_menu_layout_background"
                 android:layout_marginBottom="4dp"
-                android:layout_marginStart="8dp"
                 android:alpha="0">
                 <Button
                     android:id="@+id/maximize_menu_snap_left_button"
@@ -131,16 +146,17 @@
             </LinearLayout>
             <TextView
                 android:id="@+id/maximize_menu_snap_window_text"
-                android:layout_width="94dp"
-                android:layout_height="18dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:textSize="11sp"
-                android:layout_marginBottom="76dp"
-                android:layout_gravity="center"
+                android:lineHeight="16sp"
                 android:gravity="center"
                 android:importantForAccessibility="no"
                 android:fontFamily="google-sans-text"
+                android:textFontWeight="500"
                 android:text="@string/desktop_mode_maximize_menu_snap_text"
                 android:textColor="@androidprv:color/materialColorOnSurface"
+                android:singleLine="true"
                 android:alpha="0"/>
         </LinearLayout>
     </LinearLayout>
@@ -150,6 +166,6 @@
     <View
         android:id="@+id/maximize_menu_overlay"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/desktop_mode_maximize_menu_height"/>
+        android:layout_height="match_parent"/>
 </FrameLayout>
 
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index e395341..f5f3f0f 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -498,14 +498,6 @@
     <!-- The default minimum allowed window height when resizing a window in desktop mode. -->
     <dimen name="desktop_mode_minimum_window_height">352dp</dimen>
 
-    <!-- The width of the maximize menu in desktop mode, depending on the number of options -->
-    <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen>
-    <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen>
-    <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen>
-
-    <!-- The height of the maximize menu in desktop mode. -->
-    <dimen name="desktop_mode_maximize_menu_height">114dp</dimen>
-
     <!-- The padding of the maximize menu in desktop mode. -->
     <dimen name="desktop_mode_menu_padding">16dp</dimen>
 
diff --git a/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml
new file mode 100644
index 0000000..975d25b
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:alpha="0.35" android:color="@androidprv:color/materialColorPrimaryContainer" />
+</selector>
diff --git a/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml
new file mode 100644
index 0000000..89546f9
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <corners android:radius="28dp" />
+    <solid android:color="@color/bubble_drop_target_background_color" />
+    <stroke
+        android:width="1dp"
+        android:color="@androidprv:color/materialColorPrimaryContainer" />
+</shape>
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index d280083..11a6f32 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -36,4 +36,14 @@
     <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen>
     <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen>
     <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen>
+
+    <!-- Bubble drop target dimensions -->
+    <dimen name="drop_target_elevation">1dp</dimen>
+    <dimen name="drop_target_full_screen_padding">20dp</dimen>
+    <dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
+    <dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
+    <dimen name="drop_target_expanded_view_width">364</dimen>
+    <dimen name="drop_target_expanded_view_height">578</dimen>
+    <dimen name="drop_target_expanded_view_padding_bottom">108</dimen>
+    <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen>
 </resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 4d00c741..8519872 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -21,6 +21,7 @@
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -55,9 +56,15 @@
 public class TransitionUtil {
     /** Flag applied to a transition change to identify it as a divider bar for animation. */
     public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+    public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1;
 
     /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */
-    public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1;
+    public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2;
+
+    /**
+     * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher.
+     */
+    public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1;
 
     /** @return true if the transition was triggered by opening something vs closing something */
     public static boolean isOpeningType(@WindowManager.TransitionType int type) {
@@ -117,6 +124,11 @@
         return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
     }
 
+    /** Returns `true` if `change` is an app's dim layer. */
+    public static boolean isDimLayer(TransitionInfo.Change change) {
+        return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER);
+    }
+
     /** Returns `true` if `change` is only re-ordering. */
     public static boolean isOrderOnly(TransitionInfo.Change change) {
         return change.getMode() == TRANSIT_CHANGE
@@ -231,6 +243,14 @@
             t.setLayer(leash, Integer.MAX_VALUE);
             return;
         }
+        if (isDimLayer(change)) {
+            // When a dim layer gets reparented onto the transition root, we need to zero out its
+            // position so that it's in line with everything else on the transition root. Also,
+            // we need to set a crop because we don't want it applying MATCH_PARENT on the whole
+            // root surface.
+            t.setPosition(leash, 0, 0);
+            t.setCrop(leash, change.getEndAbsBounds());
+        }
 
         // Put all the OPEN/SHOW on top
         if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
@@ -284,14 +304,19 @@
         // Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
         setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
         t.reparent(change.getLeash(), leashSurface);
-        t.setAlpha(change.getLeash(), 1.0f);
-        t.show(change.getLeash());
+        if (!isDimLayer(change)) {
+            // Most leashes going onto the transition root should have their alpha set here to make
+            // them visible. But dim layers should be left untouched (their alpha value is their
+            // actual dim value).
+            t.setAlpha(change.getLeash(), 1.0f);
+        }
         if (!isDividerBar(change)) {
             // For divider, don't modify its inner leash position when creating the outer leash
             // for the transition. In case the position being wrong after the transition finished.
             t.setPosition(change.getLeash(), 0, 0);
         }
         t.setLayer(change.getLeash(), 0);
+        t.show(change.getLeash());
         return leashSurface;
     }
 
@@ -333,6 +358,9 @@
         if (isDividerBar(change)) {
             return getDividerTarget(change, leash);
         }
+        if (isDimLayer(change)) {
+            return getDimLayerTarget(change, leash);
+        }
 
         int taskId;
         boolean isNotInRecents;
@@ -439,6 +467,17 @@
                 TYPE_DOCK_DIVIDER);
     }
 
+    private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change,
+            SurfaceControl leash) {
+        return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
+                leash, false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
+                change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
+                null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
+                TYPE_SPLIT_SCREEN_DIM_LAYER);
+    }
+
     /**
      * Finds the "correct" root idx for a change. The change's end display is prioritized, then
      * the start display. If there is no display, it will fallback on the 0th root in the
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
index f45dc3a..e92c1eb 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
@@ -93,10 +93,21 @@
     public static final PathInterpolator SLOWDOWN_INTERPOLATOR =
             new PathInterpolator(0.5f, 1f, 0.5f, 1f);
 
+    /**
+     * An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss
+     * point. A sharp rise, followed by a steady middle, and ending with another sharp rise.
+     */
     public static final PathInterpolator DIM_INTERPOLATOR =
             new PathInterpolator(.23f, .87f, .52f, -0.11f);
 
     /**
+     * An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp
+     * rises" of {@link #DIM_INTERPOLATOR}.
+     */
+    public static final PathInterpolator FAST_DIM_INTERPOLATOR =
+            new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f);
+
+    /**
      * Use this interpolator for animating progress values coming from the back callback to get
      * the predictive-back-typical decelerate motion.
      *
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt
index 5d346c0..6eff75c 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt
@@ -31,29 +31,41 @@
 
     /** The bounds of this drag zone. */
     val bounds: Rect
+    /** The bounds of the drop target associated with this drag zone. */
+    val dropTarget: Rect?
 
     fun contains(x: Int, y: Int) = bounds.contains(x, y)
 
     /** Represents the bubble drag area on the screen. */
-    sealed class Bubble(override val bounds: Rect) : DragZone {
-        data class Left(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds)
-        data class Right(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds)
+    sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone {
+        data class Left(override val bounds: Rect, override val dropTarget: Rect) :
+            Bubble(bounds, dropTarget)
+
+        data class Right(override val bounds: Rect, override val dropTarget: Rect) :
+            Bubble(bounds, dropTarget)
     }
 
     /** Represents dragging to Desktop Window. */
-    data class DesktopWindow(override val bounds: Rect, val dropTarget: Rect) : DragZone
+    data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone
 
     /** Represents dragging to Full Screen. */
-    data class FullScreen(override val bounds: Rect, val dropTarget: Rect) : DragZone
+    data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone
 
     /** Represents dragging to dismiss. */
-    data class Dismiss(override val bounds: Rect) : DragZone
+    data class Dismiss(override val bounds: Rect) : DragZone {
+        override val dropTarget: Rect? = null
+    }
 
     /** Represents dragging to enter Split or replace a Split app. */
     sealed class Split(override val bounds: Rect) : DragZone {
+        override val dropTarget: Rect? = null
+
         data class Left(override val bounds: Rect) : Split(bounds)
+
         data class Right(override val bounds: Rect) : Split(bounds)
+
         data class Top(override val bounds: Rect) : Split(bounds)
+
         data class Bottom(override val bounds: Rect) : Split(bounds)
     }
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index 909e9d2..1a80b0f 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.Rect
+import android.util.TypedValue
 import androidx.annotation.DimenRes
 import com.android.wm.shell.shared.R
 import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
@@ -50,6 +51,60 @@
     private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
     private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
 
+    private var fullScreenDropTargetPadding = 0
+    private var desktopWindowDropTargetPaddingSmall = 0
+    private var desktopWindowDropTargetPaddingLarge = 0
+    private var expandedViewDropTargetWidth = 0
+    private var expandedViewDropTargetHeight = 0
+    private var expandedViewDropTargetPaddingBottom = 0
+    private var expandedViewDropTargetPaddingHorizontal = 0
+
+    private val fullScreenDropTarget: Rect
+        get() =
+            Rect(windowBounds).apply {
+                inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
+            }
+
+    private val desktopWindowDropTarget: Rect
+        get() =
+            Rect(windowBounds).apply {
+                if (deviceConfig.isLandscape) {
+                    inset(
+                        /* dx= */ desktopWindowDropTargetPaddingLarge,
+                        /* dy= */ desktopWindowDropTargetPaddingSmall
+                    )
+                } else {
+                    inset(
+                        /* dx= */ desktopWindowDropTargetPaddingSmall,
+                        /* dy= */ desktopWindowDropTargetPaddingLarge
+                    )
+                }
+            }
+
+    private val expandedViewDropTargetLeft: Rect
+        get() =
+            Rect(
+                expandedViewDropTargetPaddingHorizontal,
+                windowBounds.bottom -
+                    expandedViewDropTargetPaddingBottom -
+                    expandedViewDropTargetHeight,
+                expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
+                windowBounds.bottom - expandedViewDropTargetPaddingBottom
+            )
+
+    private val expandedViewDropTargetRight: Rect
+        get() =
+            Rect(
+                windowBounds.right -
+                    expandedViewDropTargetPaddingHorizontal -
+                    expandedViewDropTargetWidth,
+                windowBounds.bottom -
+                    expandedViewDropTargetPaddingBottom -
+                    expandedViewDropTargetHeight,
+                windowBounds.right - expandedViewDropTargetPaddingHorizontal,
+                windowBounds.bottom - expandedViewDropTargetPaddingBottom
+            )
+
     init {
         onConfigurationUpdated()
     }
@@ -88,11 +143,32 @@
             context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall)
         vSplitFromExpandedViewDragZoneHeightFoldShort =
             context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short)
+        fullScreenDropTargetPadding =
+            context.resolveDimension(R.dimen.drop_target_full_screen_padding)
+        desktopWindowDropTargetPaddingSmall =
+            context.resolveDimension(R.dimen.drop_target_desktop_window_padding_small)
+        desktopWindowDropTargetPaddingLarge =
+            context.resolveDimension(R.dimen.drop_target_desktop_window_padding_large)
+
+        // TODO b/393172431: Use the shared xml resources once we can easily access them from
+        //  launcher
+        expandedViewDropTargetWidth = 364.dpToPx()
+        expandedViewDropTargetHeight = 578.dpToPx()
+        expandedViewDropTargetPaddingBottom = 108.dpToPx()
+        expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
     }
 
     private fun Context.resolveDimension(@DimenRes dimension: Int) =
         resources.getDimensionPixelSize(dimension)
 
+    private fun Int.dpToPx() =
+        TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                this.toFloat(),
+                context.resources.displayMetrics
+            )
+            .toInt()
+
     /**
      * Creates the list of drag zones for the dragged object.
      *
@@ -155,7 +231,7 @@
             DragZone.Bubble.Left(
                 bounds =
                     Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
-                dropTarget = Rect(0, 0, 0, 0),
+                dropTarget = expandedViewDropTargetLeft,
             ),
             DragZone.Bubble.Right(
                 bounds =
@@ -165,7 +241,7 @@
                         windowBounds.right,
                         windowBounds.bottom,
                     ),
-                dropTarget = Rect(0, 0, 0, 0),
+                dropTarget = expandedViewDropTargetRight,
             )
         )
     }
@@ -174,7 +250,7 @@
         return listOf(
             DragZone.Bubble.Left(
                 bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
-                dropTarget = Rect(0, 0, 0, 0),
+                dropTarget = expandedViewDropTargetLeft,
             ),
             DragZone.Bubble.Right(
                 bounds =
@@ -184,7 +260,7 @@
                         windowBounds.right,
                         windowBounds.bottom,
                     ),
-                dropTarget = Rect(0, 0, 0, 0),
+                dropTarget = expandedViewDropTargetRight,
             )
         )
     }
@@ -198,7 +274,7 @@
                     windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
                     fullScreenDragZoneHeight
                 ),
-            dropTarget = Rect(0, 0, 0, 0)
+            dropTarget = fullScreenDropTarget
         )
     }
 
@@ -223,7 +299,7 @@
                         windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
                     )
                 },
-            dropTarget = Rect(0, 0, 0, 0)
+            dropTarget = desktopWindowDropTarget
         )
     }
 
@@ -236,7 +312,7 @@
                     windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
                     windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
                 ),
-            dropTarget = Rect(0, 0, 0, 0)
+            dropTarget = desktopWindowDropTarget
         )
     }
 
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
index 29ce8d9..2dc183f 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
@@ -16,22 +16,54 @@
 
 package com.android.wm.shell.shared.bubbles
 
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ValueAnimator
+
 /**
  * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
  * across different drag zones.
  */
 class DropTargetManager(
+    context: Context,
+    private val container: FrameLayout,
     private val isLayoutRtl: Boolean,
-    private val dragZoneChangedListener: DragZoneChangedListener
+    private val dragZoneChangedListener: DragZoneChangedListener,
 ) {
 
     private var state: DragState? = null
+    private val dropTargetView = View(context)
+    private var animator: ValueAnimator? = null
+
+    private companion object {
+        const val ANIMATION_DURATION_MS = 250L
+    }
 
     /** Must be called when a drag gesture is starting. */
     fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
         val state = DragState(dragZones, draggedObject)
         dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
         this.state = state
+        animator?.cancel()
+        setupDropTarget()
+    }
+
+    private fun setupDropTarget() {
+        if (dropTargetView.parent != null) container.removeView(dropTargetView)
+        container.addView(dropTargetView, 0)
+        // TODO b/393173014: set elevation and background
+        dropTargetView.alpha = 0f
+        dropTargetView.scaleX = 1f
+        dropTargetView.scaleY = 1f
+        dropTargetView.translationX = 0f
+        dropTargetView.translationY = 0f
+        // the drop target is added with a width and height of 1 pixel. when it gets resized, we use
+        // set its scale to the width and height of the bounds it should have to avoid layout passes
+        dropTargetView.layoutParams = FrameLayout.LayoutParams(/* width= */ 1, /* height= */ 1)
     }
 
     /** Called when the user drags to a new location. */
@@ -42,14 +74,67 @@
         state.currentDragZone = newDragZone
         if (oldDragZone != newDragZone) {
             dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone)
+            updateDropTarget()
         }
     }
 
     /** Called when the drag ended. */
     fun onDragEnded() {
+        startFadeAnimation(from = dropTargetView.alpha, to = 0f) {
+            container.removeView(dropTargetView)
+        }
         state = null
     }
 
+    private fun updateDropTarget() {
+        val currentDragZone = state?.currentDragZone ?: return
+        val dropTargetBounds = currentDragZone.dropTarget
+        when {
+            dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
+            dropTargetView.alpha == 0f -> {
+                dropTargetView.translationX = dropTargetBounds.exactCenterX()
+                dropTargetView.translationY = dropTargetBounds.exactCenterY()
+                dropTargetView.scaleX = dropTargetBounds.width().toFloat()
+                dropTargetView.scaleY = dropTargetBounds.height().toFloat()
+                startFadeAnimation(from = 0f, to = 1f)
+            }
+            else -> startMorphAnimation(dropTargetBounds)
+        }
+    }
+
+    private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS)
+        animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
+        if (onEnd != null) {
+            animator.doOnEnd(onEnd)
+        }
+        this.animator = animator
+        animator.start()
+    }
+
+    private fun startMorphAnimation(bounds: Rect) {
+        animator?.cancel()
+        val startAlpha = dropTargetView.alpha
+        val startTx = dropTargetView.translationX
+        val startTy = dropTargetView.translationY
+        val startScaleX = dropTargetView.scaleX
+        val startScaleY = dropTargetView.scaleY
+        val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
+        animator.addUpdateListener { _ ->
+            val fraction = animator.animatedValue as Float
+            dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
+            dropTargetView.translationX = startTx + (bounds.exactCenterX() - startTx) * fraction
+            dropTargetView.translationY = startTy + (bounds.exactCenterY() - startTy) * fraction
+            dropTargetView.scaleX =
+                startScaleX + (bounds.width().toFloat() - startScaleX) * fraction
+            dropTargetView.scaleY =
+                startScaleY + (bounds.height().toFloat() - startScaleY) * fraction
+        }
+        this.animator = animator
+        animator.start()
+    }
+
     /** Stores the current drag state. */
     private inner class DragState(
         private val dragZones: List<DragZone>,
@@ -72,7 +157,18 @@
     interface DragZoneChangedListener {
         /** An initial drag zone was set. Called when a drag starts. */
         fun onInitialDragZoneSet(dragZone: DragZone)
+
         /** Called when the object was dragged to a different drag zone. */
         fun onDragZoneChanged(from: DragZone, to: DragZone)
     }
+
+    private fun Animator.doOnEnd(onEnd: () -> Unit) {
+        addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    onEnd()
+                }
+            }
+        )
+    }
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
index 126ab3d..14338a4 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
@@ -24,7 +24,6 @@
 import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
 import android.window.DesktopModeFlags
 import com.android.internal.R
-import com.android.window.flags.Flags
 
 /**
  * Class to decide whether to apply app compat policies in desktop mode.
@@ -64,7 +63,7 @@
      * is enabled.
      */
     fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
-        Flags.excludeCaptionFromAppBounds()
+        DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue
                 && isAnyForceConsumptionFlagsEnabled()
                 && taskInfo.topActivityInfo?.let {
             isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled(
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 00901a4..00c446c 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -27,6 +27,7 @@
 import android.os.SystemProperties;
 import android.view.Display;
 import android.view.WindowManager;
+import android.window.DesktopExperienceFlags;
 import android.window.DesktopModeFlags;
 
 import com.android.internal.R;
@@ -271,7 +272,7 @@
      * frontend implementations).
      */
     public static boolean enableMultipleDesktops(@NonNull Context context) {
-        return Flags.enableMultipleDesktopsBackend()
+        return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
                 && Flags.enableMultipleDesktopsFrontend()
                 && canEnterDesktopMode(context);
     }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index b48296f5..759e711 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -262,6 +262,7 @@
 
     /** Flag applied to a transition change to identify it as a divider bar for animation. */
     public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR;
+    public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER;
 
     public static final String splitPositionToString(@SplitPosition int pos) {
         switch (pos) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index e44c895..58b46d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1295,6 +1295,11 @@
                         mContext.getResources().getDimensionPixelSize(
                                 com.android.internal.R.dimen.importance_ring_stroke_width));
                 mStackView.onDisplaySizeChanged();
+                // TODO b/392893178: Merge the unfold and the task view transition so that we don't
+                //  have to post a delayed runnable to the looper to update the bounds
+                if (mStackView.isExpanded()) {
+                    mStackView.postDelayed(() -> mStackView.updateExpandedView(), 500);
+                }
             }
             if (newConfig.fontScale != mFontScale) {
                 mFontScale = newConfig.fontScale;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index dad627f..9272417 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -3548,7 +3548,7 @@
         }
     }
 
-    private void updateExpandedView() {
+    void updateExpandedView() {
         boolean isOverflowExpanded = mExpandedBubble != null
                 && BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
         int[] paddings = mPositioner.getExpandedViewContainerPadding(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index 4c3bde9..97184c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -67,6 +67,7 @@
     private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
     private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
     private final Map<Integer, RectF> mUnpopulatedDisplayBounds = new HashMap<>();
+    private DisplayTopology mDisplayTopology;
 
     public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
             ShellExecutor mainExecutor, DisplayManager displayManager) {
@@ -157,6 +158,7 @@
             for (int i = 0; i < mDisplays.size(); ++i) {
                 listener.onDisplayAdded(mDisplays.keyAt(i));
             }
+            listener.onTopologyChanged(mDisplayTopology);
         }
     }
 
@@ -245,6 +247,7 @@
         if (topology == null) {
             return;
         }
+        mDisplayTopology = topology;
         SparseArray<RectF> absoluteBounds = topology.getAbsoluteBounds();
         mUnpopulatedDisplayBounds.clear();
         for (int i = 0; i < absoluteBounds.size(); ++i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt
new file mode 100644
index 0000000..a1d700a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 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
+
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default
+ * parameters values [builder = ::WindowContainerTransaction] which requires the
+ * [@JvmOverloads] annotation to make this available in Java.
+ * This can be used every time a component needs the dependency to the default [Supplier] for
+ * [WindowContainerTransaction]s.
+ */
+@WMSingleton
+class WindowContainerTransactionSupplier @Inject constructor(
+) : Supplier<WindowContainerTransaction> {
+    override fun get(): WindowContainerTransaction = WindowContainerTransaction()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java
new file mode 100644
index 0000000..fb2a324
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2025 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.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when
+ * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_ALIGN_CENTER} is the desired
+ * parallax effect.
+ */
+public class CenterParallaxSpec implements ParallaxSpec {
+    @Override
+    public void getParallax(Point retreatingOut, Point advancingOut, int position,
+            DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+            Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+            Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+        if (isLeftRightSplit) {
+            retreatingOut.x = (retreatingSurface.width() - retreatingContent.width()) / 2;
+        } else {
+            retreatingOut.y = (retreatingSurface.height() - retreatingContent.height()) / 2;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java
new file mode 100644
index 0000000..39ecbb3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 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 static android.view.WindowManager.DOCKED_INVALID;
+
+import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.WindowManager;
+
+/**
+ * Calculation class, used when
+ * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_DISMISSING} is the desired parallax
+ * effect.
+ */
+public class DismissingParallaxSpec implements ParallaxSpec {
+    @Override
+    public void getParallax(Point retreatingOut, Point advancingOut, int position,
+            DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+            Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+            Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+        if (dimmingSide == DOCKED_INVALID) {
+            return;
+        }
+
+        float progressTowardScreenEdge =
+                Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
+        int totalDismissingDistance = 0;
+        if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) {
+            totalDismissingDistance = snapAlgorithm.getDismissStartTarget().getPosition()
+                    - snapAlgorithm.getFirstSplitTarget().getPosition();
+        } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) {
+            totalDismissingDistance = snapAlgorithm.getLastSplitTarget().getPosition()
+                    - snapAlgorithm.getDismissEndTarget().getPosition();
+        }
+
+        float parallaxFraction =
+                calculateParallaxDismissingFraction(progressTowardScreenEdge, dimmingSide);
+        if (isLeftRightSplit) {
+            retreatingOut.x = (int) (parallaxFraction * totalDismissingDistance);
+        } else {
+            retreatingOut.y = (int) (parallaxFraction * totalDismissingDistance);
+        }
+    }
+
+    /**
+     * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
+     * slowing down parallax effect
+     */
+    private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
+        float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
+
+        // Less parallax at the top, just because.
+        if (dockSide == WindowManager.DOCKED_TOP) {
+            result /= 2f;
+        }
+        return result;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index 2f5afca..5b2dd97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -465,5 +465,9 @@
             this.snapPosition = snapPosition;
             this.distanceMultiplier = distanceMultiplier;
         }
+
+        public int getPosition() {
+            return position;
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
new file mode 100644
index 0000000..9fa1621
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2025 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 static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM;
+import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
+import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX}
+ * is the desired parallax effect.
+ */
+public class FlexParallaxSpec implements ParallaxSpec {
+    final Rect mTempRect = new Rect();
+
+    @Override
+    public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm,
+            boolean isLeftRightSplit) {
+        if (position < snapAlgorithm.getMiddleTarget().getPosition()) {
+            return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
+        } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) {
+            return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
+        }
+        return DOCKED_INVALID;
+    }
+
+    /**
+     * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split.
+     * In flexible split, there are two dimming "behaviors".
+     *   1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or
+     *      90%, we dim the app slightly as it moves partially offscreen.
+     *   2) "fast dim": when moving the divider from a side snap target further toward the screen
+     *      edge, we dim the app rapidly as it approaches the dismiss point.
+     * @return 0f = no dim applied. 1f = full black.
+     */
+    public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+        int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition();
+        int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition();
+        int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition();
+        int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition();
+        int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition();
+        float progress;
+
+        if (startDismissPos <= position && position < firstTargetPos) {
+            // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves
+            // toward the screen edge
+            progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos);
+            return fastDim(progress);
+        } else if (firstTargetPos <= position && position < middleTargetPos) {
+            // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target
+            progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos);
+            return slowDim(progress);
+        } else if (middleTargetPos <= position && position < lastTargetPos) {
+            // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target
+            progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos);
+            return slowDim(progress);
+        } else if (lastTargetPos <= position && position <= endDismissPos) {
+            // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it
+            // moves toward screen edge
+            progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos);
+            return fastDim(progress);
+        }
+        return 0f;
+    }
+
+    /**
+     * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps
+     * up to the default amount of dimming for an offscreen app,
+     * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}.
+     */
+    private float slowDim(float progress) {
+        return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM;
+    }
+
+    /**
+     * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at
+     * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black).
+     */
+    private float fastDim(float progress) {
+        return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress)
+                * (1 - DEFAULT_OFFSCREEN_DIM));
+    }
+
+    @Override
+    public void getParallax(Point retreatingOut, Point advancingOut, int position,
+            DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+            Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+            Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+        // Whether an app is getting pushed offscreen by the divider.
+        boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface);
+        // Whether an app was getting pulled onscreen at the beginning of the drag.
+        boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent);
+
+        // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10)
+        if (isRetreatingOffscreen && !advancingSideStartedOffscreen) {
+            // On the left side, we use parallax to simulate the contents sticking to the
+            // divider. This is because surfaces naturally expand to the bottom and right,
+            // so when a surface's area expands, the contents stick to the left. This is
+            // correct behavior on the right-side surface, but not the left.
+            if (topLeftShrink) {
+                if (isLeftRightSplit) {
+                    retreatingOut.x = retreatingSurface.width() - retreatingContent.width();
+                } else {
+                    retreatingOut.y = retreatingSurface.height() - retreatingContent.height();
+                }
+            }
+            // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss)
+        } else {
+            mTempRect.set(retreatingSurface);
+            Point rootOffset = new Point();
+            // 10:90 -> 50:50, 10:90, or dismiss right
+            if (advancingSideStartedOffscreen) {
+                // We have to handle a complicated case here to keep the parallax smooth.
+                // When the divider crosses the 50% mark, the retreating-side app surface
+                // will start expanding offscreen. This is expected and unavoidable, but
+                // makes the parallax look disjointed. In order to preserve the illusion,
+                // we add another offset (rootOffset) to simulate the surface staying
+                // onscreen.
+                if (mTempRect.intersect(displayBounds)) {
+                    if (retreatingSurface.left < displayBounds.left) {
+                        rootOffset.x = displayBounds.left - retreatingSurface.left;
+                    }
+                    if (retreatingSurface.top < displayBounds.top) {
+                        rootOffset.y = displayBounds.top - retreatingSurface.top;
+                    }
+                }
+
+                // On the left side, we again have to simulate the contents sticking to the
+                // divider.
+                if (!topLeftShrink) {
+                    if (isLeftRightSplit) {
+                        advancingOut.x = advancingSurface.width() - advancingContent.width();
+                    } else {
+                        advancingOut.y = advancingSurface.height() - advancingContent.height();
+                    }
+                }
+            }
+
+            // In all these cases, the shrinking app also receives a center parallax.
+            if (isLeftRightSplit) {
+                retreatingOut.x = rootOffset.x
+                        + ((mTempRect.width() - retreatingContent.width()) / 2);
+            } else {
+                retreatingOut.y = rootOffset.y
+                        + ((mTempRect.height() - retreatingContent.height()) / 2);
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java
new file mode 100644
index 0000000..043b288
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 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.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_NONE}
+ * is the desired parallax effect.
+ */
+public class NoParallaxSpec implements ParallaxSpec {
+    @Override
+    public void getParallax(Point retreatingOut, Point advancingOut, int position,
+            DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+            Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+            Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+        // no-op
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java
new file mode 100644
index 0000000..84d849b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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 static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Default interface for a set of calculation classes, used for calculating various parallax and
+ * dimming effects in split screen.
+ */
+public interface ParallaxSpec {
+    /** Returns an int indicating which side of the screen is being dimmed (if any). */
+    default int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm,
+            boolean isLeftRightSplit) {
+        if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) {
+            return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
+        } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) {
+            return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
+        }
+        return DOCKED_INVALID;
+    }
+
+    /** Returns the dim amount that we'll apply to the app surface. 0f = no dim, 1f = full black */
+    default float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+        float progressTowardScreenEdge =
+                Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
+        return DIM_INTERPOLATOR.getInterpolation(progressTowardScreenEdge);
+    }
+
+    /**
+     * Calculates the amount to offset app surfaces to create nice parallax effects. Writes to
+     * {@link ResizingEffectPolicy#mRetreatingSideParallax} and
+     * {@link ResizingEffectPolicy#mAdvancingSideParallax}.
+     */
+    void getParallax(Point retreatingOut, Point advancingOut, int position,
+            DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+            Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+            Rect advancingContent, int dimmingSide, boolean topLeftShrink);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
index 3f76fd0..e2e1f96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
@@ -26,27 +26,32 @@
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING;
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE;
-import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
-import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
 
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.view.SurfaceControl;
-import android.view.WindowManager;
 
 /**
  * This class governs how and when parallax and dimming effects are applied to task surfaces,
  * usually when the divider is being moved around by the user (or during an animation).
  */
 class ResizingEffectPolicy {
+    /** The default amount to dim an app that is partially offscreen. */
+    public static float DEFAULT_OFFSCREEN_DIM = 0.32f;
+
     private final SplitLayout mSplitLayout;
     /** The parallax algorithm we are currently using. */
     private final int mParallaxType;
+    /**
+     * A convenience class, corresponding to {@link #mParallaxType}, that performs all the
+     * calculations for parallax and dimming values.
+     */
+    private final ParallaxSpec mParallaxSpec;
 
     int mShrinkSide = DOCKED_INVALID;
 
     // The current dismissing side.
-    int mDismissingSide = DOCKED_INVALID;
+    int mDimmingSide = DOCKED_INVALID;
 
     /**
      * A {@link Point} that stores a single x and y value, representing the parallax translation
@@ -62,7 +67,7 @@
     final Point mAdvancingSideParallax = new Point();
 
     // The dimming value to hint the dismissing side and progress.
-    float mDismissingDimValue = 0.0f;
+    float mDimValue = 0.0f;
 
     /**
      * Content bounds for the app that the divider is moving toward. This is the content that is
@@ -95,35 +100,38 @@
     ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) {
         mParallaxType = parallaxType;
         mSplitLayout = splitLayout;
+        switch (mParallaxType) {
+            case PARALLAX_DISMISSING:
+                mParallaxSpec = new DismissingParallaxSpec();
+                break;
+            case PARALLAX_ALIGN_CENTER:
+                mParallaxSpec = new CenterParallaxSpec();
+                break;
+            case PARALLAX_FLEX:
+                mParallaxSpec = new FlexParallaxSpec();
+                break;
+            case PARALLAX_NONE:
+            default:
+                mParallaxSpec = new NoParallaxSpec();
+                break;
+        }
     }
 
     /**
-     * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax}
-     * and {@link #mAdvancingSideParallax}. These values will be then be applied in
-     * {@link #adjustRootSurface}.
-     *
-     * @param position    The divider's position on the screen (x-coordinate in left-right split,
-     *                    y-coordinate in top-bottom split).
+     * Calculates the desired parallax and dimming values for a task surface and stores them in
+     * {@link #mRetreatingSideParallax}, {@link #mAdvancingSideParallax}, and
+     * {@link #mDimValue} These values will be then be applied in
+     * {@link #adjustRootSurface} and {@link #adjustDimSurface} respectively.
      */
     void applyDividerPosition(
             int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) {
-        mDismissingSide = DOCKED_INVALID;
+        mDimmingSide = DOCKED_INVALID;
         mRetreatingSideParallax.set(0, 0);
         mAdvancingSideParallax.set(0, 0);
-        mDismissingDimValue = 0;
+        mDimValue = 0;
         Rect displayBounds = mSplitLayout.getRootBounds();
 
-        int totalDismissingDistance = 0;
-        if (position < snapAlgorithm.getFirstSplitTarget().position) {
-            mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
-            totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position
-                    - snapAlgorithm.getFirstSplitTarget().position;
-        } else if (position > snapAlgorithm.getLastSplitTarget().position) {
-            mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
-            totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position
-                    - snapAlgorithm.getDismissEndTarget().position;
-        }
-
+        // Figure out which side is shrinking, and assign retreating/advancing bounds
         final boolean topLeftShrink = isLeftRightSplit
                 ? position < mSplitLayout.getTopLeftContentBounds().right
                 : position < mSplitLayout.getTopLeftContentBounds().bottom;
@@ -141,106 +149,20 @@
             mAdvancingSurface.set(mSplitLayout.getTopLeftBounds());
         }
 
-        if (mDismissingSide != DOCKED_INVALID) {
-            float fraction =
-                    Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
-            mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
-            if (mParallaxType == PARALLAX_DISMISSING) {
-                fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
-                if (isLeftRightSplit) {
-                    mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance);
-                } else {
-                    mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance);
-                }
-            }
+        // Figure out if we should be dimming one side
+        mDimmingSide = mParallaxSpec.getDimmingSide(position, snapAlgorithm, isLeftRightSplit);
+
+        // If so, calculate dimming
+        if (mDimmingSide != DOCKED_INVALID) {
+            mDimValue = mParallaxSpec.getDimValue(position, snapAlgorithm);
         }
 
-        if (mParallaxType == PARALLAX_ALIGN_CENTER) {
-            if (isLeftRightSplit) {
-                mRetreatingSideParallax.x =
-                        (mRetreatingSurface.width() - mRetreatingContent.width()) / 2;
-            } else {
-                mRetreatingSideParallax.y =
-                        (mRetreatingSurface.height() - mRetreatingContent.height()) / 2;
-            }
-        } else if (mParallaxType == PARALLAX_FLEX) {
-            // Whether an app is getting pushed offscreen by the divider.
-            boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface);
-            // Whether an app was getting pulled onscreen at the beginning of the drag.
-            boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent);
-
-            // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10)
-            if (isRetreatingOffscreen && !advancingSideStartedOffscreen) {
-                // On the left side, we use parallax to simulate the contents sticking to the
-                // divider. This is because surfaces naturally expand to the bottom and right,
-                // so when a surface's area expands, the contents stick to the left. This is
-                // correct behavior on the right-side surface, but not the left.
-                if (topLeftShrink) {
-                    if (isLeftRightSplit) {
-                        mRetreatingSideParallax.x =
-                                mRetreatingSurface.width() - mRetreatingContent.width();
-                    } else {
-                        mRetreatingSideParallax.y =
-                                mRetreatingSurface.height() - mRetreatingContent.height();
-                    }
-                }
-                // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss)
-            } else {
-                mTempRect.set(mRetreatingSurface);
-                Point rootOffset = new Point();
-                // 10:90 -> 50:50, 10:90, or dismiss right
-                if (advancingSideStartedOffscreen) {
-                    // We have to handle a complicated case here to keep the parallax smooth.
-                    // When the divider crosses the 50% mark, the retreating-side app surface
-                    // will start expanding offscreen. This is expected and unavoidable, but
-                    // makes the parallax look disjointed. In order to preserve the illusion,
-                    // we add another offset (rootOffset) to simulate the surface staying
-                    // onscreen.
-                    mTempRect.intersect(displayBounds);
-                    if (mRetreatingSurface.left < displayBounds.left) {
-                        rootOffset.x = displayBounds.left - mRetreatingSurface.left;
-                    }
-                    if (mRetreatingSurface.top < displayBounds.top) {
-                        rootOffset.y = displayBounds.top - mRetreatingSurface.top;
-                    }
-
-                    // On the left side, we again have to simulate the contents sticking to the
-                    // divider.
-                    if (!topLeftShrink) {
-                        if (isLeftRightSplit) {
-                            mAdvancingSideParallax.x =
-                                    mAdvancingSurface.width() - mAdvancingContent.width();
-                        } else {
-                            mAdvancingSideParallax.y =
-                                    mAdvancingSurface.height() - mAdvancingContent.height();
-                        }
-                    }
-                }
-
-                // In all these cases, the shrinking app also receives a center parallax.
-                if (isLeftRightSplit) {
-                    mRetreatingSideParallax.x = rootOffset.x
-                            + ((mTempRect.width() - mRetreatingContent.width()) / 2);
-                } else {
-                    mRetreatingSideParallax.y = rootOffset.y
-                            + ((mTempRect.height() - mRetreatingContent.height()) / 2);
-                }
-            }
-        }
-    }
-
-    /**
-     * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
-     * slowing down parallax effect
-     */
-    private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
-        float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
-
-        // Less parallax at the top, just because.
-        if (dockSide == WindowManager.DOCKED_TOP) {
-            result /= 2f;
-        }
-        return result;
+        // Calculate parallax and modify mRetreatingSideParallax and mAdvancingSideParallax, for use
+        // in adjustRootSurface().
+        mParallaxSpec.getParallax(mRetreatingSideParallax, mAdvancingSideParallax, position,
+                snapAlgorithm, isLeftRightSplit, displayBounds, mRetreatingSurface,
+                mRetreatingContent, mAdvancingSurface, mAdvancingContent, mDimmingSide,
+                topLeftShrink);
     }
 
     /** Applies the calculated parallax and dimming values to task surfaces. */
@@ -250,7 +172,7 @@
         SurfaceControl advancingLeash = null;
 
         if (mParallaxType == PARALLAX_DISMISSING) {
-            switch (mDismissingSide) {
+            switch (mDimmingSide) {
                 case DOCKED_TOP:
                 case DOCKED_LEFT:
                     retreatingLeash = leash1;
@@ -303,14 +225,17 @@
     void adjustDimSurface(SurfaceControl.Transaction t,
             SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
         SurfaceControl targetDimLayer;
-        switch (mDismissingSide) {
+        SurfaceControl oppositeDimLayer;
+        switch (mDimmingSide) {
             case DOCKED_TOP:
             case DOCKED_LEFT:
                 targetDimLayer = dimLayer1;
+                oppositeDimLayer = dimLayer2;
                 break;
             case DOCKED_BOTTOM:
             case DOCKED_RIGHT:
                 targetDimLayer = dimLayer2;
+                oppositeDimLayer = dimLayer1;
                 break;
             case DOCKED_INVALID:
             default:
@@ -318,7 +243,9 @@
                 t.setAlpha(dimLayer2, 0).hide(dimLayer2);
                 return;
         }
-        t.setAlpha(targetDimLayer, mDismissingDimValue)
-                .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
+        t.setAlpha(targetDimLayer, mDimValue)
+                .setVisibility(targetDimLayer, mDimValue > 0.001f);
+        t.setAlpha(oppositeDimLayer, 0f)
+                .setVisibility(oppositeDimLayer, false);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index bd89f5c..708e26c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -128,6 +128,8 @@
     // The touch layer is on a stage root, and is sibling with things like the app activity itself
     // and the app veil. We want it to be above all those.
     public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE;
+    // The dim layer is also on the stage root, and stays under the touch layer.
+    public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1;
 
     // Animation specs for the swap animation
     private static final int SWAP_ANIMATION_TOTAL_DURATION = 500;
@@ -1201,6 +1203,12 @@
             // Resets layer of divider bar to make sure it is always on top.
             t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER);
         }
+        if (dimLayer1 != null) {
+            t.setLayer(dimLayer1, RESTING_DIM_LAYER);
+        }
+        if (dimLayer2 != null) {
+            t.setLayer(dimLayer2, RESTING_DIM_LAYER);
+        }
         copyTopLeftRefBounds(mTempRect);
         t.setPosition(leash1, mTempRect.left, mTempRect.top)
                 .setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
index d1d133d..ad0e7fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
@@ -57,4 +57,9 @@
     public List<RectF> getLayout(@SplitScreenState int state) {
         return mSplitSpec.getSpec(state);
     }
+
+    /** Returns the layout associated with the current split state. */
+    public List<RectF> getCurrentLayout() {
+        return getLayout(mState);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt
new file mode 100644
index 0000000..bdffcf5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 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.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.window.WindowContainerToken
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+
+/**
+ * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the
+ * Letterbox Input surface, understands the type of event and filter them based on the current
+ * letterbox position.
+ */
+class ReachabilityGestureListener(
+    private val taskId: Int,
+    private val token: WindowContainerToken?,
+    private val transitions: Transitions,
+    private val animationHandler: Transitions.TransitionHandler,
+    private val wctSupplier: WindowContainerTransactionSupplier
+) : GestureDetector.SimpleOnGestureListener() {
+
+    // The current letterbox bounds. Double tap events are ignored when happening in these bounds.
+    private val activityBounds = Rect()
+
+    override fun onDoubleTap(e: MotionEvent): Boolean {
+        val x = e.rawX.toInt()
+        val y = e.rawY.toInt()
+        if (!activityBounds.contains(x, y)) {
+            val wct = wctSupplier.get().apply {
+                setReachabilityOffset(token!!, taskId, x, y)
+            }
+            transitions.startTransition(
+                TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+                wct,
+                animationHandler
+            )
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Updates the bounds for the letterboxed activity.
+     */
+    fun updateActivityBounds(newActivityBounds: Rect) {
+        activityBounds.set(newActivityBounds)
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt
new file mode 100644
index 0000000..5e9fe09
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 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.compatui.letterbox.events
+
+import android.window.WindowContainerToken
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.dagger.WMSingleton
+import com.android.wm.shell.transition.Transitions
+import javax.inject.Inject
+
+/**
+ * A Factory for [ReachabilityGestureListener].
+ */
+@WMSingleton
+class ReachabilityGestureListenerFactory @Inject constructor(
+    private val transitions: Transitions,
+    private val animationHandler: Transitions.TransitionHandler,
+    private val wctSupplier: WindowContainerTransactionSupplier
+) {
+    /**
+     * @return a [ReachabilityGestureListener] implementation to listen to double tap events and
+     * creating the related [WindowContainerTransaction] to handle the transition.
+     */
+    fun createReachabilityGestureListener(
+        taskId: Int,
+        token: WindowContainerToken?
+    ): ReachabilityGestureListener =
+        ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 35475c7..2fd8c27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -759,7 +759,6 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            DesktopTilingDecorViewModel desktopTilingDecorViewModel,
             DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
             Optional<BubbleController> bubbleController,
             OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
@@ -798,7 +797,6 @@
                 mainHandler,
                 desktopModeEventLogger,
                 desktopModeUiEventLogger,
-                desktopTilingDecorViewModel,
                 desktopWallpaperActivityTokenProvider,
                 bubbleController,
                 overviewToDesktopTransitionObserver,
@@ -990,7 +988,8 @@
             DesktopModeUiEventLogger desktopModeUiEventLogger,
             WindowDecorTaskResourceLoader taskResourceLoader,
             RecentsTransitionHandler recentsTransitionHandler,
-            DesktopModeCompatPolicy desktopModeCompatPolicy
+            DesktopModeCompatPolicy desktopModeCompatPolicy,
+            DesktopTilingDecorViewModel desktopTilingDecorViewModel
     ) {
         if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
             return Optional.empty();
@@ -1006,7 +1005,8 @@
                 desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
                 windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
                 focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
-                taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy));
+                taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy,
+                desktopTilingDecorViewModel));
     }
 
     @WMSingleton
@@ -1278,10 +1278,10 @@
     @WMSingleton
     @Provides
     static DesktopWindowingEducationTooltipController
-            provideDesktopWindowingEducationTooltipController(
-                    Context context,
-                    AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
-                    DisplayController displayController) {
+    provideDesktopWindowingEducationTooltipController(
+            Context context,
+            AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
+            DisplayController displayController) {
         return new DesktopWindowingEducationTooltipController(
                 context, additionalSystemViewContainerFactory, displayController);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 7d80ee5..f8b18f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -53,6 +53,7 @@
 import com.android.wm.shell.pip2.phone.PipTransitionState;
 import com.android.wm.shell.pip2.phone.PipUiStateChangeController;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -85,11 +86,13 @@
             @NonNull PipDisplayLayoutState pipDisplayLayoutState,
             @NonNull PipUiStateChangeController pipUiStateChangeController,
             DisplayController displayController,
+            Optional<SplitScreenController> splitScreenControllerOptional,
             PipDesktopState pipDesktopState) {
         return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
                 pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
                 pipScheduler, pipStackListenerController, pipDisplayLayoutState,
-                pipUiStateChangeController, displayController, pipDesktopState);
+                pipUiStateChangeController, displayController, splitScreenControllerOptional,
+                pipDesktopState);
     }
 
     @WMSingleton
@@ -140,9 +143,10 @@
             PipBoundsState pipBoundsState,
             @ShellMainThread ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
+            Optional<SplitScreenController> splitScreenControllerOptional,
             PipDesktopState pipDesktopState) {
         return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
-                pipDesktopState);
+                splitScreenControllerOptional, pipDesktopState);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index 6f455df..c38558d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -26,6 +26,7 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.IWindowManager
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
 import android.window.WindowContainerTransaction
 import com.android.internal.protolog.ProtoLog
 import com.android.window.flags.Flags
@@ -62,7 +63,7 @@
     private fun onInit() {
         displayController.addDisplayWindowListener(this)
 
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
             desktopTasksController.onDeskRemovedListener = this
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index 03bc42f..0cc8a6a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.desktopmode
 
-import com.android.window.flags.Flags
+import android.window.DesktopExperienceFlags
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
 import com.android.wm.shell.sysui.ShellCommandHandler
 import java.io.PrintWriter
@@ -56,7 +56,7 @@
                 pw.println("Error: task id should be an integer")
                 return false
             }
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN)
         }
         if (args.size < 3) {
@@ -95,7 +95,7 @@
     }
 
     private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -116,7 +116,7 @@
     }
 
     private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -137,7 +137,7 @@
     }
 
     private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -158,7 +158,7 @@
     }
 
     private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -167,7 +167,7 @@
     }
 
     private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -188,7 +188,7 @@
     }
 
     private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -204,12 +204,12 @@
                 pw.println("Error: task id should be an integer")
                 return false
             }
-        pw.println("Not implemented.")
-        return false
+        controller.moveToFullscreen(taskId, transitionSource = UNKNOWN)
+        return true
     }
 
     private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -225,7 +225,7 @@
     }
 
     private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("Not supported.")
             return false
         }
@@ -246,7 +246,7 @@
     }
 
     override fun printShellCommandHelp(pw: PrintWriter, prefix: String) {
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             pw.println("$prefix moveTaskToDesk <taskId> ")
             pw.println("$prefix  Move a task with given id to desktop mode.")
             pw.println("$prefix moveToNextDisplay <taskId> ")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 9b850de6..c5ee313 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -33,6 +33,7 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayLayout
+import kotlin.math.ceil
 
 val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float =
     SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
@@ -190,22 +191,22 @@
     val finalWidth: Int
     // Get orientation either through top activity or task's orientation
     if (taskInfo.hasPortraitTopActivity()) {
-        val tempWidth = (targetHeight / aspectRatio).toInt()
+        val tempWidth = ceil(targetHeight / aspectRatio).toInt()
         if (tempWidth <= targetWidth) {
             finalHeight = targetHeight
             finalWidth = tempWidth
         } else {
             finalWidth = targetWidth
-            finalHeight = (finalWidth * aspectRatio).toInt()
+            finalHeight = ceil(finalWidth * aspectRatio).toInt()
         }
     } else {
-        val tempWidth = (targetHeight * aspectRatio).toInt()
+        val tempWidth = ceil(targetHeight * aspectRatio).toInt()
         if (tempWidth <= targetWidth) {
             finalHeight = targetHeight
             finalWidth = tempWidth
         } else {
             finalWidth = targetWidth
-            finalHeight = (finalWidth / aspectRatio).toInt()
+            finalHeight = ceil(finalWidth / aspectRatio).toInt()
         }
     }
     return Size(finalWidth, finalHeight + captionInsets)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index aecbf1a..99f05283 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -214,6 +214,13 @@
         return result;
     }
 
+    /**
+     * Returns the [DragStartState] of the visual indicator.
+     */
+    DragStartState getDragStartState() {
+        return mDragStartState;
+    }
+
     @VisibleForTesting
     Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) {
         final Region region = new Region();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 4777e7f..eba1be5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -22,6 +22,7 @@
 import android.util.ArraySet
 import android.util.SparseArray
 import android.view.Display.INVALID_DISPLAY
+import android.window.DesktopExperienceFlags
 import android.window.DesktopModeFlags
 import androidx.core.util.forEach
 import androidx.core.util.valueIterator
@@ -137,7 +138,7 @@
     private var desktopGestureExclusionExecutor: Executor? = null
 
     private val desktopData: DesktopData =
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             MultiDesktopData()
         } else {
             SingleDesktopData()
@@ -226,10 +227,19 @@
         desktopData.setActiveDesk(displayId = displayId, deskId = deskId)
     }
 
+    /** Sets the given desk as inactive if it was active. */
+    fun setDeskInactive(deskId: Int) {
+        desktopData.setDeskInactive(deskId)
+    }
+
     /** Returns the id of the active desk in the given display, if any. */
     @VisibleForTesting
     fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId
 
+    /** Returns the id of the desk to which this task belongs. */
+    fun getDeskIdForTask(taskId: Int): Int? =
+        desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId
+
     /**
      * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk.
      *
@@ -270,20 +280,40 @@
     @VisibleForTesting
     fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) {
         val affectedDisplays = mutableSetOf<Int>()
-        desktopData.forAllDesks { displayId, desk ->
-            if (desk.deskId != excludedDeskId && desk.activeTasks.remove(taskId)) {
-                logD(
-                    "Removed active task=%d displayId=%d deskId=%d",
-                    taskId,
-                    displayId,
-                    desk.deskId,
-                )
-                affectedDisplays.add(displayId)
+        desktopData
+            .desksSequence()
+            .filter { desk -> desk.displayId != excludedDeskId }
+            .forEach { desk ->
+                val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false)
+                if (removed) {
+                    logD(
+                        "Removed active task=%d displayId=%d deskId=%d",
+                        taskId,
+                        desk.displayId,
+                        desk.deskId,
+                    )
+                    affectedDisplays.add(desk.displayId)
+                }
             }
-        }
         affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) }
     }
 
+    private fun removeActiveTaskFromDesk(
+        deskId: Int,
+        taskId: Int,
+        notifyListeners: Boolean = true,
+    ): Boolean {
+        val desk = desktopData.getDesk(deskId) ?: return false
+        if (desk.activeTasks.remove(taskId)) {
+            logD("Removed active task=%d from deskId=%d", taskId, desk.deskId)
+            if (notifyListeners) {
+                updateActiveTasksListeners(desk.displayId)
+            }
+            return true
+        }
+        return false
+    }
+
     /**
      * Adds given task to the closing task list for [displayId]'s active desk.
      *
@@ -322,10 +352,22 @@
 
     fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks }
 
+    @VisibleForTesting
+    fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean {
+        val desk = desktopData.getDesk(deskId) ?: return false
+        return taskId in desk.activeTasks
+    }
+
     fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks }
 
     fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks }
 
+    @VisibleForTesting
+    fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean {
+        val desk = desktopData.getDesk(deskId) ?: return false
+        return taskId in desk.visibleTasks
+    }
+
     fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks }
 
     /**
@@ -415,12 +457,19 @@
     /** Removes task from visible tasks of all desks except [excludedDeskId]. */
     private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) {
         desktopData.forAllDesks { displayId, desk ->
-            if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) {
-                notifyVisibleTaskListeners(displayId, desk.visibleTasks.size)
+            if (desk.deskId != excludedDeskId) {
+                removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId)
             }
         }
     }
 
+    private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) {
+        val desk = desktopData.getDesk(deskId) ?: return
+        if (desk.visibleTasks.remove(taskId)) {
+            notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size)
+        }
+    }
+
     /**
      * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners.
      *
@@ -576,15 +625,26 @@
     /**
      * Set whether the given task is the full-immersive task in this display's active desk.
      *
-     * TODO: b/389960283 - add explicit [deskId] argument.
+     * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with
+     *   an explicit desk id instead of using this function and defaulting to the active one.
      */
     fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
-        val desktopData = desktopData.getActiveDesk(displayId) ?: return
+        val activeDesk = desktopData.getActiveDesk(displayId) ?: return
+        setTaskInFullImmersiveStateInDesk(
+            deskId = activeDesk.deskId,
+            taskId = taskId,
+            immersive = immersive,
+        )
+    }
+
+    /** Sets whether the given task is the full-immersive task in the given desk. */
+    fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) {
+        val desk = desktopData.getDesk(deskId) ?: return
         if (immersive) {
-            desktopData.fullImmersiveTaskId = taskId
+            desk.fullImmersiveTaskId = taskId
         } else {
-            if (desktopData.fullImmersiveTaskId == taskId) {
-                desktopData.fullImmersiveTaskId = null
+            if (desk.fullImmersiveTaskId == taskId) {
+                desk.fullImmersiveTaskId = null
             }
         }
     }
@@ -674,7 +734,8 @@
     /**
      * Minimizes the task for [taskId] and [displayId]'s active display.
      *
-     * TODO: b/389960283 - add explicit [deskId] argument.
+     * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit
+     *   desk id instead of using this function and defaulting to the active one.
      */
     fun minimizeTask(displayId: Int, taskId: Int) {
         if (displayId == INVALID_DISPLAY) {
@@ -683,32 +744,41 @@
             getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) }
                 ?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
             return
-        } else {
-            logD("Minimize Task: display=%d, task=%d", displayId, taskId)
-            desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId)
-                ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
         }
-        updateTask(displayId, taskId, isVisible = false)
+        val deskId = desktopData.getActiveDesk(displayId)?.deskId
+        if (deskId == null) {
+            logD("Minimize task: No active desk found for task: taskId=%d", taskId)
+            return
+        }
+        minimizeTaskInDesk(displayId, deskId, taskId)
+    }
+
+    /** Minimizes the task in its desk. */
+    @VisibleForTesting
+    fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
+        logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
+        desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
+            ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
+        updateTaskInDesk(displayId, deskId, taskId, isVisible = false)
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
-            updatePersistentRepository(displayId)
+            updatePersistentRepositoryForDesk(deskId)
         }
     }
 
     /**
      * Unminimizes the task for [taskId] and [displayId].
      *
-     * TODO: b/389960283 - consider adding an explicit [deskId] argument.
+     * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead.
      */
     fun unminimizeTask(displayId: Int, taskId: Int) {
         logD("Unminimize Task: display=%d, task=%d", displayId, taskId)
-        var removed = false
-        desktopData.forAllDesks(displayId) { desk ->
-            if (desk.minimizedTasks.remove(taskId)) {
-                removed = true
-            }
-        }
-        if (!removed) {
-            logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId)
+        desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) }
+    }
+
+    private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) {
+        logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId)
+        if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) {
+            logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId)
         }
     }
 
@@ -729,7 +799,7 @@
      * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
      * will be looked up from the task id.
      *
-     * TODO: b/389960283 - consider adding an explicit [deskId] argument.
+     * TODO: b/389960283 - consider using [removeTaskFromDesk] instead.
      */
     fun removeTask(displayId: Int, taskId: Int) {
         logD("Removes freeform task: taskId=%d", taskId)
@@ -745,24 +815,33 @@
     private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
         logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
         desktopData.forAllDesks(displayId) { desk ->
-            if (desk.freeformTasksInZOrder.remove(taskId)) {
-                logD(
-                    "Remaining freeform tasks in desk: %d, tasks: %s",
-                    desk.deskId,
-                    desk.freeformTasksInZOrder.toDumpString(),
-                )
-            }
+            removeTaskFromDesk(deskId = desk.deskId, taskId = taskId)
         }
+    }
+
+    /** Removes the given task from the given desk. */
+    fun removeTaskFromDesk(deskId: Int, taskId: Int) {
+        logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId)
+        // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving
+        //  it between desks. It might be better to allow restoring to the previous bounds as long
+        //  as they're valid (probably valid if in the same display).
         boundsBeforeMaximizeByTaskId.remove(taskId)
         boundsBeforeFullImmersiveByTaskId.remove(taskId)
-        // Remove task from unminimized task if it is minimized.
-        unminimizeTask(displayId, taskId)
+        val desk = desktopData.getDesk(deskId) ?: return
+        if (desk.freeformTasksInZOrder.remove(taskId)) {
+            logD(
+                "Remaining freeform tasks in desk: %d, tasks: %s",
+                desk.deskId,
+                desk.freeformTasksInZOrder.toDumpString(),
+            )
+        }
+        unminimizeTaskFromDesk(deskId, taskId)
         // Mark task as not in immersive if it was immersive.
-        setTaskInFullImmersiveState(displayId = displayId, taskId = taskId, immersive = false)
-        removeActiveTask(taskId)
-        removeVisibleTask(taskId)
-        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
-            updatePersistentRepository(displayId)
+        setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false)
+        removeActiveTaskFromDesk(deskId = deskId, taskId = taskId)
+        removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId)
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+            updatePersistentRepositoryForDesk(desk.deskId)
         }
     }
 
@@ -832,24 +911,29 @@
     private fun updatePersistentRepository(displayId: Int) {
         val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
         mainCoroutineScope.launch {
-            desks.forEach { desk ->
-                try {
-                    persistentRepository.addOrUpdateDesktop(
-                        // Use display id as desk id for now since only once desk per display
-                        // is supported.
-                        userId = userId,
-                        desktopId = desk.deskId,
-                        visibleTasks = desk.visibleTasks,
-                        minimizedTasks = desk.minimizedTasks,
-                        freeformTasksInZOrder = desk.freeformTasksInZOrder,
-                    )
-                } catch (exception: Exception) {
-                    logE(
-                        "An exception occurred while updating the persistent repository \n%s",
-                        exception.stackTrace,
-                    )
-                }
-            }
+            desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) }
+        }
+    }
+
+    private fun updatePersistentRepositoryForDesk(deskId: Int) {
+        val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return
+        mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) }
+    }
+
+    private suspend fun updatePersistentRepositoryForDesk(desk: Desk) {
+        try {
+            persistentRepository.addOrUpdateDesktop(
+                userId = userId,
+                desktopId = desk.deskId,
+                visibleTasks = desk.visibleTasks,
+                minimizedTasks = desk.minimizedTasks,
+                freeformTasksInZOrder = desk.freeformTasksInZOrder,
+            )
+        } catch (exception: Exception) {
+            logE(
+                "An exception occurred while updating the persistent repository \n%s",
+                exception.stackTrace,
+            )
         }
     }
 
@@ -866,21 +950,27 @@
         desktopData
             .desksSequence()
             .groupBy { it.displayId }
-            .forEach { (displayId, desks) ->
+            .map { (displayId, desks) ->
+                Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks)
+            }
+            .forEach { (displayId, activeDeskId, desks) ->
                 pw.println("${prefix}Display #$displayId:")
+                pw.println("${innerPrefix}activeDesk=$activeDeskId")
+                pw.println("${innerPrefix}desks:")
+                val desksPrefix = "$innerPrefix  "
                 desks.forEach { desk ->
-                    pw.println("${innerPrefix}Desk #${desk.deskId}:")
-                    pw.print("$innerPrefix  activeTasks=")
+                    pw.println("${desksPrefix}Desk #${desk.deskId}:")
+                    pw.print("$desksPrefix  activeTasks=")
                     pw.println(desk.activeTasks.toDumpString())
-                    pw.print("$innerPrefix  visibleTasks=")
+                    pw.print("$desksPrefix  visibleTasks=")
                     pw.println(desk.visibleTasks.toDumpString())
-                    pw.print("$innerPrefix  freeformTasksInZOrder=")
+                    pw.print("$desksPrefix  freeformTasksInZOrder=")
                     pw.println(desk.freeformTasksInZOrder.toDumpString())
-                    pw.print("$innerPrefix  minimizedTasks=")
+                    pw.print("$desksPrefix  minimizedTasks=")
                     pw.println(desk.minimizedTasks.toDumpString())
-                    pw.print("$innerPrefix  fullImmersiveTaskId=")
+                    pw.print("$desksPrefix  fullImmersiveTaskId=")
                     pw.println(desk.fullImmersiveTaskId)
-                    pw.print("$innerPrefix  topTransparentFullscreenTaskId=")
+                    pw.print("$desksPrefix  topTransparentFullscreenTaskId=")
                     pw.println(desk.topTransparentFullscreenTaskId)
                 }
             }
@@ -910,6 +1000,9 @@
         /** Sets the given desk as the active desk in the given display. */
         fun setActiveDesk(displayId: Int, deskId: Int)
 
+        /** Sets the desk as inactive if it was active. */
+        fun setDeskInactive(deskId: Int)
+
         /**
          * Returns the default desk in the given display. Useful when the system wants to activate a
          * desk but doesn't care about which one it activates (e.g. when putting a window into a
@@ -990,6 +1083,11 @@
             // existence of visible desktop windows, among other factors.
         }
 
+        override fun setDeskInactive(deskId: Int) {
+            // No-op, in single-desk setups, which desktop is "active" is determined by the
+            // existence of visible desktop windows, among other factors.
+        }
+
         override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId)
 
         override fun getAllActiveDesks(): Set<Desk> =
@@ -1058,6 +1156,14 @@
             display.activeDeskId = desk.deskId
         }
 
+        override fun setDeskInactive(deskId: Int) {
+            desktopDisplays.forEach { id, display ->
+                if (display.activeDeskId == deskId) {
+                    display.activeDeskId = null
+                }
+            }
+        }
+
         override fun getDefaultDesk(displayId: Int): Desk? {
             val display = desktopDisplays[displayId] ?: return null
             return display.orderedDesks.find { it.deskId == display.activeDeskId }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
index 4d87b21..e831d5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
@@ -42,6 +42,12 @@
             desktopUserRepositories.getProfile(taskInfo.userId)
         if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
 
+        // TODO: b/394281403 - with multiple desks, it's possible to have a non-freeform task
+        //  inside a desk, so this should be decoupled from windowing mode.
+        //  Also, changes in/out of desks are handled by the [DesksTransitionObserver], which has
+        //  more specific information about the desk involved in the transition, which might be
+        //  more accurate than assuming it's always the default/active desk in the display, as this
+        //  method does.
         // Case 1: Freeform task is changed in Desktop Mode.
         if (isFreeformTask(taskInfo)) {
             if (taskInfo.isVisible) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index bbf0010..f7fe694 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -41,6 +41,7 @@
 import android.os.IBinder
 import android.os.SystemProperties
 import android.os.UserHandle
+import android.util.Slog
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
 import android.view.MotionEvent
@@ -53,6 +54,7 @@
 import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.widget.Toast
+import android.window.DesktopExperienceFlags
 import android.window.DesktopModeFlags
 import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER
@@ -136,7 +138,6 @@
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -144,7 +145,7 @@
 import com.android.wm.shell.windowdecor.extension.isFullscreen
 import com.android.wm.shell.windowdecor.extension.isMultiWindow
 import com.android.wm.shell.windowdecor.extension.requestingImmersive
-import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
 import java.io.PrintWriter
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -152,6 +153,16 @@
 import java.util.function.Consumer
 import kotlin.jvm.optionals.getOrNull
 
+/**
+ * A callback to be invoked when a transition is started via |Transitions.startTransition| with the
+ * transition binder token that it produces.
+ *
+ * Useful when multiple components are appending WCT operations to a single transition that is
+ * started outside of their control, and each of them wants to track the transition lifecycle
+ * independently by cross-referencing the transition token with future ready-transitions.
+ */
+typealias RunOnTransitStart = (IBinder) -> Unit
+
 /** Handles moving tasks in and out of desktop */
 class DesktopTasksController(
     private val context: Context,
@@ -184,7 +195,6 @@
     @ShellMainThread private val handler: Handler,
     private val desktopModeEventLogger: DesktopModeEventLogger,
     private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
-    private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
     private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
     private val bubbleController: Optional<BubbleController>,
     private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
@@ -204,7 +214,9 @@
     private var userId: Int
     private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
         DesktopModeShellCommandHandler(this)
+
     private val mOnAnimationFinishedCallback = { releaseVisualIndicator() }
+    private lateinit var snapEventHandler: SnapEventHandler
     private val dragToDesktopStateListener =
         object : DragToDesktopStateListener {
             override fun onCommitToDesktopAnimationStart() {
@@ -269,7 +281,7 @@
                         RecentsTransitionStateListener.stateToString(state),
                     )
                     recentsTransitionState = state
-                    desktopTilingDecorViewModel.onOverviewAnimationStateChange(
+                    snapEventHandler.onOverviewAnimationStateChange(
                         RecentsTransitionStateListener.isAnimating(state)
                     )
                 }
@@ -300,6 +312,11 @@
         dragToDesktopTransitionHandler.setSplitScreenController(controller)
     }
 
+    /** Setter to handle snap events */
+    fun setSnapEventHandler(handler: SnapEventHandler) {
+        snapEventHandler = handler
+    }
+
     /** Returns the transition type for the given remote transition. */
     private fun transitionType(remoteTransition: RemoteTransition?): Int {
         if (remoteTransition == null) {
@@ -423,7 +440,7 @@
 
     /** Creates a new desk in the given display. */
     fun createDesk(displayId: Int) {
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             desksOrganizer.createDesk(displayId) { deskId ->
                 taskRepository.addDesk(displayId = displayId, deskId = deskId)
             }
@@ -450,10 +467,7 @@
         }
         // TODO(342378842): Instead of using default display, support multiple displays
         val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY
-        val deskId =
-            checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
-                "Expected a default desk to exist"
-            }
+        val deskId = getDefaultDeskId(displayId)
         return moveTaskToDesk(
             taskId = taskId,
             deskId = deskId,
@@ -474,7 +488,7 @@
     ): Boolean {
         val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId)
         if (runningTask != null) {
-            moveRunningTaskToDesk(
+            return moveRunningTaskToDesk(
                 task = runningTask,
                 deskId = deskId,
                 wct = wct,
@@ -556,10 +570,10 @@
         transitionSource: DesktopModeTransitionSource,
         remoteTransition: RemoteTransition? = null,
         callback: IMoveToDesktopCallback? = null,
-    ) {
+    ): Boolean {
         if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) {
             logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId)
-            return
+            return false
         }
         val displayId = taskRepository.getDisplayForDesk(deskId)
         logV(
@@ -602,7 +616,7 @@
             addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT)
         }
         exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             desksTransitionObserver.addPendingTransition(
                 DeskTransition.ActiveDeskWithTask(
                     token = transition,
@@ -614,6 +628,7 @@
         } else {
             taskRepository.setActiveDesk(displayId = displayId, deskId = deskId)
         }
+        return true
     }
 
     /**
@@ -630,7 +645,7 @@
         task: RunningTaskInfo,
     ): Int? {
         val taskIdToMinimize =
-            if (Flags.enableMultipleDesktopsBackend()) {
+            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                 // Activate the desk first.
                 prepareForDeskActivation(displayId, wct)
                 desksOrganizer.activateDesk(wct, deskId)
@@ -650,7 +665,7 @@
                 // Bring other apps to front first.
                 bringDesktopAppsToFrontBeforeShowingNewTask(displayId, wct, task.taskId)
             }
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             prepareMoveTaskToDesk(wct, task, deskId)
         } else {
             addMoveToDesktopChanges(wct, task)
@@ -697,10 +712,7 @@
      * [startDragToDesktop].
      */
     private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
-        val deskId =
-            checkNotNull(taskRepository.getDefaultDeskId(taskInfo.displayId)) {
-                "Expected a default desk to exist"
-            }
+        val deskId = getDefaultDeskId(taskInfo.displayId)
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
             "DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d",
@@ -709,7 +721,7 @@
         )
         val wct = WindowContainerTransaction()
         exitSplitIfApplicable(wct, taskInfo)
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             // |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so
             // this shouldn't be necessary at all.
             if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
@@ -741,7 +753,7 @@
                 addPendingMinimizeTransition(it, taskId, MinimizeReason.TASK_LIMIT)
             }
             exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
-            if (Flags.enableMultipleDesktopsBackend()) {
+            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                 desksTransitionObserver.addPendingTransition(
                     DeskTransition.ActiveDeskWithTask(
                         token = transition,
@@ -782,22 +794,44 @@
         wct: WindowContainerTransaction,
         displayId: Int,
         taskInfo: RunningTaskInfo,
-    ): ((IBinder) -> Unit)? {
+    ): ((IBinder) -> Unit) {
         val taskId = taskInfo.taskId
-        desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
+        val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+        snapEventHandler.removeTaskIfTiled(displayId, taskId)
+        val shouldExitDesktop =
+            willExitDesktop(
+                triggerTaskId = taskInfo.taskId,
+                displayId = displayId,
+                forceToFullscreen = false,
+            )
+        taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
+        val desktopExitRunnable =
+            performDesktopExitCleanUp(
+                wct = wct,
+                deskId = deskId,
+                displayId = displayId,
+                willExitDesktop = shouldExitDesktop,
+                shouldEndUpAtHome = true,
+            )
+
         taskRepository.addClosingTask(displayId, taskId)
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(displayId, taskId)
         )
-        return desktopImmersiveController
-            .exitImmersiveIfApplicable(
-                wct = wct,
-                taskInfo = taskInfo,
-                reason = DesktopImmersiveController.ExitReason.CLOSED,
-            )
-            .asExit()
-            ?.runOnTransitionStart
+
+        val immersiveRunnable =
+            desktopImmersiveController
+                .exitImmersiveIfApplicable(
+                    wct = wct,
+                    taskInfo = taskInfo,
+                    reason = DesktopImmersiveController.ExitReason.CLOSED,
+                )
+                .asExit()
+                ?.runOnTransitionStart
+        return { transitionToken ->
+            immersiveRunnable?.invoke(transitionToken)
+            desktopExitRunnable?.invoke(transitionToken)
+        }
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
@@ -831,10 +865,20 @@
 
     private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
         val taskId = taskInfo.taskId
+        val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
         val displayId = taskInfo.displayId
         val wct = WindowContainerTransaction()
-        desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
+
+        snapEventHandler.removeTaskIfTiled(displayId, taskId)
+        taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
+        val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+        val desktopExitRunnable =
+            performDesktopExitCleanUp(
+                wct = wct,
+                deskId = deskId,
+                displayId = displayId,
+                willExitDesktop = willExitDesktop,
+            )
         // Notify immersive handler as it might need to exit immersive state.
         val exitResult =
             desktopImmersiveController.exitImmersiveIfApplicable(
@@ -856,12 +900,13 @@
             )
         }
         exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
+        desktopExitRunnable?.invoke(transition)
     }
 
     /** Move a task with given `taskId` to fullscreen */
     fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
-            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId)
+            snapEventHandler.removeTaskIfTiled(task.displayId, taskId)
             moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
         }
     }
@@ -869,7 +914,7 @@
     /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
     fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
         getFocusedFreeformTask(displayId)?.let {
-            desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId)
+            snapEventHandler.removeTaskIfTiled(displayId, it.taskId)
             moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
         }
     }
@@ -903,7 +948,8 @@
     ) {
         logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
         val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task)
+        val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+        val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
 
         // We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
         if (!forceEnterDesktop(task.displayId)) {
@@ -911,12 +957,14 @@
             wct.reorder(task.token, /* onTop= */ true)
         }
 
-        exitDesktopTaskTransitionHandler.startTransition(
-            transitionSource,
-            wct,
-            position,
-            mOnAnimationFinishedCallback,
-        )
+        val transition =
+            exitDesktopTaskTransitionHandler.startTransition(
+                transitionSource,
+                wct,
+                position,
+                mOnAnimationFinishedCallback,
+            )
+        deactivationRunnable?.invoke(transition)
 
         // handles case where we are moving to full screen without closing all DW tasks.
         if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
@@ -988,7 +1036,7 @@
         logV("moveTaskToFront taskId=%s", taskInfo.taskId)
         // If a task is tiled, another task should be brought to foreground with it so let
         // tiling controller handle the request.
-        if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) {
+        if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) {
             return
         }
         val wct = WindowContainerTransaction()
@@ -1175,6 +1223,8 @@
             wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
         }
 
+        // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going
+        //  home.
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
             performDesktopExitCleanupIfNeeded(
                 task.taskId,
@@ -1230,7 +1280,7 @@
         } else {
             // Save current bounds so that task can be restored back to original bounds if necessary
             // and toggle to the stable bounds.
-            desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
+            snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
             taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
             destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
         }
@@ -1356,7 +1406,6 @@
         position: SnapPosition,
         resizeTrigger: ResizeTrigger,
         inputMethod: InputMethod,
-        desktopWindowDecoration: DesktopModeWindowDecoration,
     ) {
         desktopModeEventLogger.logTaskResizingStarted(
             resizeTrigger,
@@ -1378,13 +1427,7 @@
         )
 
         if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
-            val isTiled =
-                desktopTilingDecorViewModel.snapToHalfScreen(
-                    taskInfo,
-                    desktopWindowDecoration,
-                    position,
-                    currentDragBounds,
-                )
+            val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position)
             if (isTiled) {
                 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
             }
@@ -1421,7 +1464,6 @@
         position: SnapPosition,
         resizeTrigger: ResizeTrigger,
         inputMethod: InputMethod,
-        desktopModeWindowDecoration: DesktopModeWindowDecoration,
     ) {
         if (!isSnapResizingAllowed(taskInfo)) {
             Toast.makeText(
@@ -1440,7 +1482,6 @@
             position,
             resizeTrigger,
             inputMethod,
-            desktopModeWindowDecoration,
         )
     }
 
@@ -1452,7 +1493,6 @@
         currentDragBounds: Rect,
         dragStartBounds: Rect,
         motionEvent: MotionEvent,
-        desktopModeWindowDecoration: DesktopModeWindowDecoration,
     ) {
         releaseVisualIndicator()
         if (!isSnapResizingAllowed(taskInfo)) {
@@ -1500,7 +1540,6 @@
                 position,
                 resizeTrigger,
                 DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
-                desktopModeWindowDecoration,
             )
         }
     }
@@ -1549,7 +1588,7 @@
     private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) {
         // Move home to front, ensures that we go back home when all desktop windows are closed
         val useParamDisplayId =
-            Flags.enableMultipleDesktopsBackend() ||
+            DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
                 Flags.enablePerDisplayDesktopWallpaperActivity()
         moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct)
         // Currently, we only handle the desktop on the default display really.
@@ -1732,33 +1771,59 @@
         }
     }
 
-    /**
-     * Remove wallpaper activity if task provided is last task and wallpaper activity token is not
-     * null
-     */
-    private fun performDesktopExitCleanupIfNeeded(
-        taskId: Int,
+    private fun willExitDesktop(
+        triggerTaskId: Int,
         displayId: Int,
-        wct: WindowContainerTransaction,
         forceToFullscreen: Boolean,
-        shouldEndUpAtHome: Boolean = true,
-    ) {
-        taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen)
+    ): Boolean {
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
-            if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) {
-                return
+            if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
+                return false
             }
         } else if (
             Flags.enableDesktopWindowingPip() &&
                 taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
                 !forceToFullscreen
         ) {
-            return
+            return false
         } else {
-            if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
-                return
+            if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) {
+                return false
             }
         }
+        return true
+    }
+
+    private fun performDesktopExitCleanupIfNeeded(
+        taskId: Int,
+        displayId: Int,
+        wct: WindowContainerTransaction,
+        forceToFullscreen: Boolean,
+        shouldEndUpAtHome: Boolean = true,
+    ): RunOnTransitStart? {
+        taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen)
+        if (!willExitDesktop(taskId, displayId, forceToFullscreen)) {
+            return null
+        }
+        // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the
+        //  |RunOnTransitStart| when the transition is started.
+        return performDesktopExitCleanUp(
+            wct = wct,
+            deskId = null,
+            displayId = displayId,
+            willExitDesktop = true,
+            shouldEndUpAtHome = shouldEndUpAtHome,
+        )
+    }
+
+    private fun performDesktopExitCleanUp(
+        wct: WindowContainerTransaction,
+        deskId: Int?,
+        displayId: Int,
+        willExitDesktop: Boolean,
+        shouldEndUpAtHome: Boolean = true,
+    ): RunOnTransitStart? {
+        if (!willExitDesktop) return null
         desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
             FULLSCREEN_ANIMATION_DURATION
         )
@@ -1768,6 +1833,7 @@
             // intent.
             addLaunchHomePendingIntent(wct, displayId)
         }
+        return prepareDeskDeactivationIfNeeded(wct, deskId)
     }
 
     fun releaseVisualIndicator() {
@@ -1978,8 +2044,10 @@
                     unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON,
                 )
             } else {
-                moveBackgroundTaskToDesktop(
+                val deskId = getDefaultDeskId(callingTask.displayId)
+                moveTaskToDesk(
                     requestedTaskId,
+                    deskId,
                     WindowContainerTransaction(),
                     DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
                 )
@@ -2097,7 +2165,16 @@
     ): WindowContainerTransaction? {
         logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch")
         val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task)
+        addMoveToFullscreenChanges(
+            wct = wct,
+            taskInfo = task,
+            willExitDesktop =
+                willExitDesktop(
+                    triggerTaskId = task.taskId,
+                    displayId = task.displayId,
+                    forceToFullscreen = true,
+                ),
+        )
         wct.reorder(task.token, true)
         return wct
     }
@@ -2121,7 +2198,16 @@
                 // launched. We should make this task go to fullscreen instead of freeform. Note
                 // that this means any re-launch of a freeform window outside of desktop will be in
                 // fullscreen as long as default-desktop flag is disabled.
-                addMoveToFullscreenChanges(wct, task)
+                addMoveToFullscreenChanges(
+                    wct = wct,
+                    taskInfo = task,
+                    willExitDesktop =
+                        willExitDesktop(
+                            triggerTaskId = task.taskId,
+                            displayId = task.displayId,
+                            forceToFullscreen = true,
+                        ),
+                )
                 return wct
             }
             bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -2171,7 +2257,7 @@
             return wct
         }
         if (!wct.isEmpty) {
-            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+            snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
             return wct
         }
         return null
@@ -2217,7 +2303,16 @@
             // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED
             // set when needed can interfere with future split / multi-instance transitions.
             return WindowContainerTransaction().also { wct ->
-                addMoveToFullscreenChanges(wct, task)
+                addMoveToFullscreenChanges(
+                    wct = wct,
+                    taskInfo = task,
+                    willExitDesktop =
+                        willExitDesktop(
+                            triggerTaskId = task.taskId,
+                            displayId = task.displayId,
+                            forceToFullscreen = true,
+                        ),
+                )
             }
         }
         return null
@@ -2245,10 +2340,25 @@
         }
         // Already fullscreen, no-op.
         if (task.isFullscreen) return null
-        return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
+        return WindowContainerTransaction().also { wct ->
+            addMoveToFullscreenChanges(
+                wct = wct,
+                taskInfo = task,
+                willExitDesktop =
+                    willExitDesktop(
+                        triggerTaskId = task.taskId,
+                        displayId = task.displayId,
+                        forceToFullscreen = true,
+                    ),
+            )
+        }
     }
 
-    /** Handle task closing by removing wallpaper activity if it's the last active task */
+    /**
+     * Handle task closing by removing wallpaper activity if it's the last active task.
+     *
+     * TODO: b/394268248 - desk needs to be deactivated.
+     */
     private fun handleTaskClosing(
         task: RunningTaskInfo,
         transition: IBinder,
@@ -2267,7 +2377,7 @@
 
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
-            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+            snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
         }
 
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
@@ -2315,7 +2425,7 @@
         taskInfo: RunningTaskInfo,
         deskId: Int,
     ) {
-        if (!Flags.enableMultipleDesktopsBackend()) return
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
         val displayId = taskRepository.getDisplayForDesk(deskId)
         val displayLayout = displayController.getDisplayLayout(displayId) ?: return
         val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId)
@@ -2399,10 +2509,15 @@
         return bounds
     }
 
+    /**
+     * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to
+     * be deactivated.
+     */
     private fun addMoveToFullscreenChanges(
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo,
-    ) {
+        willExitDesktop: Boolean,
+    ): RunOnTransitStart? {
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode =
@@ -2417,12 +2532,16 @@
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
-
-        performDesktopExitCleanupIfNeeded(
-            taskInfo.taskId,
-            taskInfo.displayId,
-            wct,
-            forceToFullscreen = true,
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+            wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true)
+        }
+        taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false)
+        val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+        return performDesktopExitCleanUp(
+            wct = wct,
+            deskId = deskId,
+            displayId = taskInfo.displayId,
+            willExitDesktop = willExitDesktop,
             shouldEndUpAtHome = false,
         )
     }
@@ -2447,6 +2566,8 @@
     /**
      * Adds split screen changes to a transaction. Note that bounds are not reset here due to
      * animation; see {@link onDesktopSplitSelectAnimComplete}
+     *
+     * TODO: b/394268248 - desk needs to be deactivated.
      */
     private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
         // This windowing mode is to get the transition animation started; once we complete
@@ -2536,10 +2657,7 @@
         displayId: Int,
         remoteTransition: RemoteTransition? = null,
     ) {
-        val deskId =
-            checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
-                "Expected a default desk to exist"
-            }
+        val deskId = getDefaultDeskId(displayId)
         activateDesk(deskId, remoteTransition)
     }
 
@@ -2547,7 +2665,7 @@
     fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) {
         val displayId = taskRepository.getDisplayForDesk(deskId)
         val wct = WindowContainerTransaction()
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             prepareForDeskActivation(displayId, wct)
             desksOrganizer.activateDesk(wct, deskId)
             if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
@@ -2568,7 +2686,7 @@
 
         val transition = transitions.startTransition(transitionType, wct, handler)
         handler?.setTransition(transition)
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             desksTransitionObserver.addPendingTransition(
                 DeskTransition.ActivateDesk(
                     token = transition,
@@ -2583,16 +2701,35 @@
         )
     }
 
+    /**
+     * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home.
+     */
+    private fun prepareDeskDeactivationIfNeeded(
+        wct: WindowContainerTransaction,
+        deskId: Int?,
+    ): RunOnTransitStart? {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null
+        if (deskId == null) return null
+        desksOrganizer.deactivateDesk(wct, deskId)
+        return { transition ->
+            desksTransitionObserver.addPendingTransition(
+                DeskTransition.DeactivateDesk(token = transition, deskId = deskId)
+            )
+        }
+    }
+
     /** Removes the default desk in the given display. */
     @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()"))
     fun removeDefaultDeskInDisplay(displayId: Int) {
-        val deskId =
-            checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
-                "Expected a default desk to exist"
-            }
+        val deskId = getDefaultDeskId(displayId)
         removeDesk(displayId = displayId, deskId = deskId)
     }
 
+    private fun getDefaultDeskId(displayId: Int) =
+        checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
+            "Expected a default desk to exist in display: $displayId"
+        }
+
     /** Removes the given desk. */
     fun removeDesk(deskId: Int) {
         val displayId = taskRepository.getDisplayForDesk(deskId)
@@ -2604,7 +2741,7 @@
         logV("removeDesk deskId=%d from displayId=%d", deskId, displayId)
 
         val tasksToRemove =
-            if (Flags.enableMultipleDesktopsBackend()) {
+            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                 taskRepository.getActiveTaskIdsInDesk(deskId)
             } else {
                 // TODO: 362720497 - make sure minimized windows are also removed in WM
@@ -2613,7 +2750,7 @@
             }
 
         val wct = WindowContainerTransaction()
-        if (!Flags.enableMultipleDesktopsBackend()) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             tasksToRemove.forEach {
                 val task = shellTaskOrganizer.getRunningTaskInfo(it)
                 if (task != null) {
@@ -2626,9 +2763,9 @@
             // TODO: 362720497 - double check background tasks are also removed.
             desksOrganizer.removeDesk(wct, deskId)
         }
-        if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return
         val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null)
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             desksTransitionObserver.addPendingTransition(
                 DeskTransition.RemoveDesk(
                     token = transition,
@@ -2732,7 +2869,7 @@
         taskBounds: Rect,
     ) {
         if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
-        desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
+        snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
         updateVisualIndicator(
             taskInfo,
             taskSurface,
@@ -2749,6 +2886,12 @@
         taskTop: Float,
         dragStartState: DragStartState,
     ): DesktopModeVisualIndicator.IndicatorType {
+        // If the visual indicator has the wrong start state, it was never cleared from a previous
+        // drag event and needs to be cleared
+        if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) {
+            Slog.e(TAG, "Visual indicator from previous motion event was never released")
+            releaseVisualIndicator()
+        }
         // If the visual indicator does not exist, create it.
         val indicator =
             visualIndicator
@@ -2792,7 +2935,6 @@
         validDragArea: Rect,
         dragStartBounds: Rect,
         motionEvent: MotionEvent,
-        desktopModeWindowDecoration: DesktopModeWindowDecoration,
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
             return
@@ -2831,7 +2973,6 @@
                     currentDragBounds,
                     dragStartBounds,
                     motionEvent,
-                    desktopModeWindowDecoration,
                 )
             }
             IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
@@ -2846,7 +2987,6 @@
                     currentDragBounds,
                     dragStartBounds,
                     motionEvent,
-                    desktopModeWindowDecoration,
                 )
             }
             IndicatorType.NO_INDICATOR,
@@ -3132,7 +3272,7 @@
         logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId)
         userId = newUserId
         taskRepository = userRepositories.getProfile(userId)
-        desktopTilingDecorViewModel.onUserChange()
+        snapEventHandler.onUserChange()
     }
 
     /** Called when a task's info changes. */
@@ -3302,11 +3442,15 @@
         }
 
         override fun createDesk(displayId: Int) {
-            // TODO: b/362720497 - Implement this API.
+            executeRemoteCallWithTaskPermission(controller, "createDesk") { c ->
+                c.createDesk(displayId)
+            }
         }
 
         override fun activateDesk(deskId: Int, remoteTransition: RemoteTransition?) {
-            // TODO: b/362720497 - Implement this API.
+            executeRemoteCallWithTaskPermission(controller, "activateDesk") { c ->
+                c.activateDesk(deskId, remoteTransition)
+            }
         }
 
         override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index aaecf8c..0929ae1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -251,7 +251,8 @@
                 (cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
                     cancelState == CancelState.CANCEL_BUBBLE_RIGHT)
         ) {
-            if (!bubbleController.isPresent) {
+            if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
+                // TODO(b/388853233): add support for dragging split task to bubble
                 startCancelAnimation()
             } else {
                 // Animation is handled by BubbleController
@@ -497,6 +498,11 @@
             state.cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
                 state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT
         ) {
+            if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
+                // TODO(b/388853233): add support for dragging split task to bubble
+                startCancelDragToDesktopTransition()
+                return true
+            }
             val taskInfo =
                 state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.")
             val wct = WindowContainerTransaction()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index 5ae1fca..95cc1e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -106,7 +106,7 @@
      * @param position               Position of the task when transition is started
      * @param onAnimationEndCallback to be called after animation
      */
-    public void startTransition(@NonNull DesktopModeTransitionSource transitionSource,
+    public IBinder startTransition(@NonNull DesktopModeTransitionSource transitionSource,
             @NonNull WindowContainerTransaction wct, Point position,
             Function0<Unit> onAnimationEndCallback) {
         mPosition = position;
@@ -114,6 +114,7 @@
         final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource),
                 wct, this);
         mPendingTransitionTokens.add(token);
+        return token;
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
index 2a8a347..b5490cb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
@@ -20,7 +20,7 @@
 import android.util.SparseBooleanArray
 import android.view.Display.DEFAULT_DISPLAY
 import android.window.WindowContainerToken
-import androidx.core.util.forEach
+import androidx.core.util.keyIterator
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 
@@ -45,11 +45,13 @@
     }
 
     fun removeToken(token: WindowContainerToken) {
-        wallpaperActivityTokenByDisplayId.forEach { displayId, value ->
-            if (value == token) {
-                logV("Remove desktop wallpaper activity token for display %s", displayId)
-                wallpaperActivityTokenByDisplayId.delete(displayId)
+        val displayId =
+            wallpaperActivityTokenByDisplayId.keyIterator().asSequence().find {
+                wallpaperActivityTokenByDisplayId[it] == token
             }
+        if (displayId != null) {
+            logV("Remove desktop wallpaper activity token for display %s", displayId)
+            wallpaperActivityTokenByDisplayId.delete(displayId)
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
index 8c4fd9d..9dec969 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
@@ -42,4 +42,7 @@
         val deskId: Int,
         val enterTaskId: Int,
     ) : DeskTransition()
+
+    /** A transition to deactivate a desk. */
+    data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition()
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index 547890a..0f2f371 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -27,6 +27,9 @@
     /** Activates the given desk, making it visible in its display. */
     fun activateDesk(wct: WindowContainerTransaction, deskId: Int)
 
+    /** Deactivates the given desk, removing it as the default launch container for new tasks. */
+    fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int)
+
     /** Removes the given desk and its desktop windows. */
     fun removeDesk(wct: WindowContainerTransaction, deskId: Int)
 
@@ -37,6 +40,9 @@
         task: ActivityManager.RunningTaskInfo,
     )
 
+    /** Whether the change is for the given desk id. */
+    fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean
+
     /**
      * Returns the desk id in which the task in the given change is located at the end of a
      * transition, if any.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
index 6d88c33..e57b563 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
@@ -17,9 +17,11 @@
 
 import android.os.IBinder
 import android.view.WindowManager.TRANSIT_CLOSE
+import android.window.DesktopExperienceFlags
 import android.window.TransitionInfo
-import com.android.window.flags.Flags
+import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 
 /**
  * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It
@@ -33,7 +35,7 @@
 
     /** Adds a pending desk transition to be tracked. */
     fun addPendingTransition(transition: DeskTransition) {
-        if (!Flags.enableMultipleDesktopsBackend()) return
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
         deskTransitions[transition.token] = transition
     }
 
@@ -42,8 +44,9 @@
      * observer.
      */
     fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
-        if (!Flags.enableMultipleDesktopsBackend()) return
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
         val deskTransition = deskTransitions.remove(transition) ?: return
+        logD("Desk transition ready: %s", deskTransition)
         val desktopRepository = desktopUserRepositories.current
         when (deskTransition) {
             is DeskTransition.RemoveDesk -> {
@@ -88,6 +91,42 @@
                     )
                 }
             }
+            is DeskTransition.DeactivateDesk -> {
+                var visibleDeactivation = false
+                for (change in info.changes) {
+                    val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId)
+                    if (isDeskChange) {
+                        visibleDeactivation = true
+                        continue
+                    }
+                    val taskId = change.taskInfo?.taskId ?: continue
+                    val removedFromDesk =
+                        desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId &&
+                            desksOrganizer.getDeskAtEnd(change) == null
+                    if (removedFromDesk) {
+                        desktopRepository.removeTaskFromDesk(
+                            deskId = deskTransition.deskId,
+                            taskId = taskId,
+                        )
+                    }
+                }
+                // Always deactivate even if there's no change that confirms the desk was
+                // deactivated. Some interactions, such as the desk deactivating because it's
+                // occluded by a fullscreen task result in a transition change, but others, such
+                // as transitioning from an empty desk to home may not.
+                if (!visibleDeactivation) {
+                    logD("Deactivating desk without transition change")
+                }
+                desktopRepository.setDeskInactive(deskId = deskTransition.deskId)
+            }
         }
     }
+
+    private fun logD(msg: String, vararg arguments: Any?) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
+    private companion object {
+        private const val TAG = "DesksTransitionObserver"
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index 5cda76e..339932c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -23,12 +23,12 @@
 import android.util.SparseArray
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.DesktopExperienceFlags
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
 import androidx.core.util.forEach
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -47,7 +47,7 @@
     @VisibleForTesting val roots = SparseArray<DeskRoot>()
 
     init {
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             shellInit.addInitCallback(
                 { shellCommandHandler.addDumpCallback(this::dump, this) },
                 this,
@@ -83,6 +83,16 @@
         )
     }
 
+    override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
+        logV("deactivateDesk %d", deskId)
+        val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
+        wct.setLaunchRoot(
+            /* container= */ root.taskInfo.token,
+            /* windowingModes= */ null,
+            /* activityTypes= */ null,
+        )
+    }
+
     override fun moveTaskToDesk(
         wct: WindowContainerTransaction,
         deskId: Int,
@@ -93,6 +103,9 @@
         wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
     }
 
+    override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean =
+        roots.contains(deskId) && change.taskInfo?.taskId == deskId
+
     override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
         change.taskInfo?.parentTaskId?.takeIf { it in roots }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index 5a89451..0507e59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -17,8 +17,8 @@
 package com.android.wm.shell.desktopmode.persistence
 
 import android.content.Context
+import android.window.DesktopExperienceFlags
 import android.window.DesktopModeFlags
-import com.android.window.flags.Flags
 import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopUserRepositories
 import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -58,7 +58,7 @@
                     repository.addDesk(
                         displayId = persistentDesktop.displayId,
                         deskId =
-                            if (Flags.enableMultipleDesktopsBackend()) {
+                            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                                 persistentDesktop.desktopId
                             } else {
                                 // When disabled, desk ids are always the display id.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index da31810..cef18f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -145,7 +145,7 @@
     /**
      * Called when the Shell wants to start an exit-via-expand from Pip transition/animation.
      */
-    public void startExpandTransition(WindowContainerTransaction out) {
+    public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
         // Default implementation does nothing.
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
index 7169759..a837e7d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
@@ -296,6 +296,7 @@
             return;
         }
 
+        mMagneticTarget.updateLocationOnScreen();
         createOrUpdateDismissTarget();
 
         if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index e17587f..df7a25a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -35,6 +35,10 @@
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+
+import java.util.Optional;
 
 /**
  * Scheduler for Shell initiated PiP transitions and animations.
@@ -47,6 +51,7 @@
     private final ShellExecutor mMainExecutor;
     private final PipTransitionState mPipTransitionState;
     private final PipDesktopState mPipDesktopState;
+    private final Optional<SplitScreenController> mSplitScreenControllerOptional;
     private PipTransitionController mPipTransitionController;
     private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
             mSurfaceControlTransactionFactory;
@@ -59,12 +64,14 @@
             PipBoundsState pipBoundsState,
             ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
+            Optional<SplitScreenController> splitScreenControllerOptional,
             PipDesktopState pipDesktopState) {
         mContext = context;
         mPipBoundsState = pipBoundsState;
         mMainExecutor = mainExecutor;
         mPipTransitionState = pipTransitionState;
         mPipDesktopState = pipDesktopState;
+        mSplitScreenControllerOptional = splitScreenControllerOptional;
 
         mSurfaceControlTransactionFactory =
                 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
@@ -96,10 +103,23 @@
     public void scheduleExitPipViaExpand() {
         mMainExecutor.execute(() -> {
             if (!mPipTransitionState.isInPip()) return;
-            WindowContainerTransaction wct = getExitPipViaExpandTransaction();
-            if (wct != null) {
-                mPipTransitionController.startExpandTransition(wct);
-            }
+
+            final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction();
+            if (expandWct == null) return;
+
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            mSplitScreenControllerOptional.ifPresent(splitScreenController -> {
+                int lastParentTaskId = mPipTransitionState.getPipTaskInfo()
+                        .lastParentTaskIdBeforePip;
+                if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) {
+                    splitScreenController.prepareEnterSplitScreen(wct,
+                            null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED);
+                }
+            });
+
+            boolean toSplit = !wct.isEmpty();
+            wct.merge(expandWct, true /* transfer */);
+            mPipTransitionController.startExpandTransition(wct, toSplit);
         });
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 9adaa36..035c93d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.pip2.phone;
 
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Surface.ROTATION_0;
@@ -29,7 +28,13 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
 import static com.android.wm.shell.transition.Transitions.transitTypeToString;
@@ -45,7 +50,6 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -70,11 +74,14 @@
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
 import com.android.wm.shell.pip2.animation.PipEnterAnimator;
-import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.transition.PipExpandHandler;
 import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.Optional;
+
 /**
  * Implementation of transitions for PiP on phone.
  */
@@ -130,6 +137,7 @@
     //
     // Internal state and relevant cached info
     //
+    private final PipExpandHandler mExpandHandler;
 
     private Transitions.TransitionFinishCallback mFinishCallback;
 
@@ -151,6 +159,7 @@
             PipDisplayLayoutState pipDisplayLayoutState,
             PipUiStateChangeController pipUiStateChangeController,
             DisplayController displayController,
+            Optional<SplitScreenController> splitScreenControllerOptional,
             PipDesktopState pipDesktopState) {
         super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
                 pipBoundsAlgorithm);
@@ -165,6 +174,9 @@
         mDisplayController = displayController;
         mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
         mPipDesktopState = pipDesktopState;
+
+        mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm,
+                pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional);
     }
 
     @Override
@@ -184,10 +196,11 @@
     //
 
     @Override
-    public void startExpandTransition(WindowContainerTransaction out) {
+    public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
         if (out == null) return;
         mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
-        mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this);
+        mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT
+                : TRANSIT_EXIT_PIP, out, this);
     }
 
     @Override
@@ -239,10 +252,11 @@
             @NonNull SurfaceControl.Transaction finishT,
             @NonNull IBinder mergeTarget,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        // Just jump-cut the current animation if any, but do not merge.
         if (info.getType() == TRANSIT_EXIT_PIP) {
             end();
         }
+        mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget,
+                finishCallback);
     }
 
     @Override
@@ -290,7 +304,8 @@
                     finishCallback);
         } else if (transition == mExitViaExpandTransition) {
             mExitViaExpandTransition = null;
-            return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
+            return mExpandHandler.startAnimation(transition, info, startTransaction,
+                    finishTransaction, finishCallback);
         } else if (transition == mResizeTransition) {
             mResizeTransition = null;
             return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
@@ -436,7 +451,7 @@
                             (destinationBounds.height() - overlaySize) / 2f);
         }
 
-        final int delta = getFixedRotationDelta(info, pipChange);
+        final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
         if (delta != ROTATION_0) {
             // Update transition target changes in place to prepare for fixed rotation.
             handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -496,7 +511,7 @@
         final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange,
                 pipActivityChange);
 
-        final int delta = getFixedRotationDelta(info, pipChange);
+        final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
         if (delta != ROTATION_0) {
             // Update transition target changes in place to prepare for fixed rotation.
             handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -585,27 +600,6 @@
                 endBounds.top + activityEndOffset.y);
     }
 
-    private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
-        final Rect endBounds = outPipTaskChange.getEndAbsBounds();
-        final int width = endBounds.width();
-        final int height = endBounds.height();
-        final int left = endBounds.left;
-        final int top = endBounds.top;
-        int newTop, newLeft;
-
-        if (delta == Surface.ROTATION_90) {
-            newLeft = top;
-            newTop = -(left + width);
-        } else {
-            newLeft = -(height + top);
-            newTop = left;
-        }
-        // Modify the endBounds, rotating and placing them potentially off-screen, so that
-        // as we translate and rotate around the origin, we place them right into the target.
-        endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
-    }
-
-
     private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
@@ -633,83 +627,6 @@
         return true;
     }
 
-    private boolean startExpandAnimation(@NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
-
-        TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
-        if (pipChange == null) {
-            // pipChange is null, check to see if we've reparented the PIP activity for
-            // the multi activity case. If so we should use the activity leash instead
-            for (TransitionInfo.Change change : info.getChanges()) {
-                if (change.getTaskInfo() == null
-                        && change.getLastParent() != null
-                        && change.getLastParent().equals(pipToken)) {
-                    pipChange = change;
-                    break;
-                }
-            }
-
-            // failsafe
-            if (pipChange == null) {
-                return false;
-            }
-        }
-        mFinishCallback = finishCallback;
-
-        // The parent change if we were in a multi-activity PiP; null if single activity PiP.
-        final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
-                ? getChangeByToken(info, pipChange.getParent()) : null;
-        if (parentBeforePip != null) {
-            // For multi activity, we need to manually set the leash layer
-            startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
-        }
-
-        final Rect startBounds = pipChange.getStartAbsBounds();
-        final Rect endBounds = pipChange.getEndAbsBounds();
-        final SurfaceControl pipLeash = getLeash(pipChange);
-
-        PictureInPictureParams params = null;
-        if (pipChange.getTaskInfo() != null) {
-            // single activity
-            params = getPipParams(pipChange);
-        } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
-            // multi activity
-            params = getPipParams(parentBeforePip);
-        }
-        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
-                startBounds);
-
-        // We define delta = startRotation - endRotation, so we need to flip the sign.
-        final int delta = -getFixedRotationDelta(info, pipChange);
-        if (delta != ROTATION_0) {
-            // Update PiP target change in place to prepare for fixed rotation;
-            handleExpandFixedRotation(pipChange, delta);
-        }
-
-        PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash,
-                startTransaction, finishTransaction, endBounds, startBounds, endBounds,
-                sourceRectHint, delta);
-        animator.setAnimationEndCallback(() -> {
-            if (parentBeforePip != null) {
-                // TODO b/377362511: Animate local leash instead to also handle letterbox case.
-                // For multi-activity, set the crop to be null
-                finishTransaction.setCrop(pipLeash, null);
-            }
-            finishTransition();
-        });
-        cacheAndStartTransitionAnimator(animator);
-
-        // Save the PiP bounds in case, we re-enter the PiP with the same component.
-        float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
-                mPipBoundsState.getBounds());
-        mPipBoundsState.saveReentryState(snapFraction);
-
-        return true;
-    }
-
     private boolean startRemoveAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
@@ -743,29 +660,6 @@
     // Various helpers to resolve transition requests and infos
     //
 
-    @Nullable
-    private TransitionInfo.Change getPipChange(TransitionInfo info) {
-        for (TransitionInfo.Change change : info.getChanges()) {
-            if (change.getTaskInfo() != null
-                    && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
-                return change;
-            }
-        }
-        return null;
-    }
-
-    @Nullable
-    private TransitionInfo.Change getChangeByToken(TransitionInfo info,
-            WindowContainerToken token) {
-        for (TransitionInfo.Change change : info.getChanges()) {
-            if (change.getTaskInfo() != null
-                    && change.getTaskInfo().getToken().equals(token)) {
-                return change;
-            }
-        }
-        return null;
-    }
-
     @NonNull
     private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change pipTaskChange,
@@ -789,8 +683,8 @@
             Rect cutoutInsets = parentBeforePip != null
                     ? parentBeforePip.getTaskInfo().displayCutoutInsets
                     : pipTaskChange.getTaskInfo().displayCutoutInsets;
-            if (cutoutInsets != null
-                    && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
+            if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange,
+                    mPipDisplayLayoutState) == ROTATION_90) {
                 adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
             }
             if (mPipDesktopState.isDesktopWindowingPipEnabled()) {
@@ -807,25 +701,6 @@
         return adjustedSourceRectHint;
     }
 
-    @Surface.Rotation
-    private int getFixedRotationDelta(@NonNull TransitionInfo info,
-            @NonNull TransitionInfo.Change pipChange) {
-        TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
-        int startRotation = pipChange.getStartRotation();
-        if (pipChange.getEndRotation() != ROTATION_UNDEFINED
-                && startRotation != pipChange.getEndRotation()) {
-            // If PiP change was collected along with the display change and the orientation change
-            // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
-            return ROTATION_0;
-        }
-
-        int endRotation = fixedRotationChange != null
-                ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation();
-        int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
-                : startRotation - endRotation;
-        return delta;
-    }
-
     private void prepareOtherTargetTransforms(TransitionInfo info,
             SurfaceControl.Transaction startTransaction,
             SurfaceControl.Transaction finishTransaction) {
@@ -853,7 +728,8 @@
 
         // If PiP is enabled on Connected Displays, update PipDisplayLayoutState to have the correct
         // display info that PiP is entering in.
-        if (mPipDesktopState.isConnectedDisplaysPipEnabled()) {
+        if (mPipDesktopState.isConnectedDisplaysPipEnabled()
+                && pipTask.displayId != mPipDisplayLayoutState.getDisplayId()) {
             final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(
                     pipTask.displayId);
             if (displayLayout != null) {
@@ -1012,20 +888,6 @@
         mTransitionAnimator.start();
     }
 
-    @NonNull
-    private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
-        return pipChange.getTaskInfo().pictureInPictureParams != null
-                ? pipChange.getTaskInfo().pictureInPictureParams
-                : new PictureInPictureParams.Builder().build();
-    }
-
-    @NonNull
-    private static SurfaceControl getLeash(TransitionInfo.Change change) {
-        SurfaceControl leash = change.getLeash();
-        Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
-        return leash;
-    }
-
     //
     // Miscellaneous callbacks and listeners
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index 8805cbb..18c9a70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -314,7 +314,8 @@
         mSwipePipToHomeAppBounds.setEmpty();
     }
 
-    @Nullable WindowContainerToken getPipTaskToken() {
+    @Nullable
+    public WindowContainerToken getPipTaskToken() {
         return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
new file mode 100644
index 0000000..db4942b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2025 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.pip2.phone.transition;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PictureInPictureParams;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+public class PipExpandHandler implements Transitions.TransitionHandler {
+    private final Context mContext;
+    private final PipBoundsState mPipBoundsState;
+    private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+    private final PipTransitionState mPipTransitionState;
+    private final PipDisplayLayoutState mPipDisplayLayoutState;
+    private final Optional<SplitScreenController> mSplitScreenControllerOptional;
+
+    @Nullable
+    private Transitions.TransitionFinishCallback mFinishCallback;
+    @Nullable
+    private ValueAnimator mTransitionAnimator;
+
+    private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier;
+
+    public PipExpandHandler(Context context,
+            PipBoundsState pipBoundsState,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipTransitionState pipTransitionState,
+            PipDisplayLayoutState pipDisplayLayoutState,
+            Optional<SplitScreenController> splitScreenControllerOptional) {
+        mContext = context;
+        mPipBoundsState = pipBoundsState;
+        mPipBoundsAlgorithm = pipBoundsAlgorithm;
+        mPipTransitionState = pipTransitionState;
+        mPipDisplayLayoutState = pipDisplayLayoutState;
+        mSplitScreenControllerOptional = splitScreenControllerOptional;
+
+        mPipExpandAnimatorSupplier = PipExpandAnimator::new;
+    }
+
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request) {
+        // All Exit-via-Expand from PiP transitions are Shell initiated.
+        return null;
+    }
+
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        switch (info.getType()) {
+            case TRANSIT_EXIT_PIP:
+                return startExpandAnimation(info, startTransaction, finishTransaction,
+                        finishCallback);
+            case TRANSIT_EXIT_PIP_TO_SPLIT:
+                return startExpandToSplitAnimation(info, startTransaction, finishTransaction,
+                        finishCallback);
+        }
+        return false;
+    }
+
+    @Override
+    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        end();
+    }
+
+    /**
+     * Ends the animation if such is running in the context of expanding out of PiP.
+     */
+    public void end() {
+        if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) {
+            mTransitionAnimator.end();
+            mTransitionAnimator = null;
+        }
+    }
+
+    private boolean startExpandAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+        TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
+        if (pipChange == null) {
+            // pipChange is null, check to see if we've reparented the PIP activity for
+            // the multi activity case. If so we should use the activity leash instead
+            for (TransitionInfo.Change change : info.getChanges()) {
+                if (change.getTaskInfo() == null
+                        && change.getLastParent() != null
+                        && change.getLastParent().equals(pipToken)) {
+                    pipChange = change;
+                    break;
+                }
+            }
+
+            // failsafe
+            if (pipChange == null) {
+                return false;
+            }
+        }
+        mFinishCallback = finishCallback;
+
+        // The parent change if we were in a multi-activity PiP; null if single activity PiP.
+        final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
+                ? getChangeByToken(info, pipChange.getParent()) : null;
+        if (parentBeforePip != null) {
+            // For multi activity, we need to manually set the leash layer
+            startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
+        }
+
+        final Rect startBounds = pipChange.getStartAbsBounds();
+        final Rect endBounds = pipChange.getEndAbsBounds();
+        final SurfaceControl pipLeash = getLeash(pipChange);
+
+        PictureInPictureParams params = null;
+        if (pipChange.getTaskInfo() != null) {
+            // single activity
+            params = getPipParams(pipChange);
+        } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
+            // multi activity
+            params = getPipParams(parentBeforePip);
+        }
+        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
+                startBounds);
+
+        // We define delta = startRotation - endRotation, so we need to flip the sign.
+        final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
+        if (delta != ROTATION_0) {
+            // Update PiP target change in place to prepare for fixed rotation;
+            handleExpandFixedRotation(pipChange, delta);
+        }
+
+        PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+                startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+                sourceRectHint, delta);
+        animator.setAnimationEndCallback(() -> {
+            if (parentBeforePip != null) {
+                // TODO b/377362511: Animate local leash instead to also handle letterbox case.
+                // For multi-activity, set the crop to be null
+                finishTransaction.setCrop(pipLeash, null);
+            }
+            finishTransition();
+        });
+        cacheAndStartTransitionAnimator(animator);
+        saveReentryState();
+        return true;
+    }
+
+    private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+        // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP
+        // and the lastParentBeforePip is still in one of the split-stages.
+        //
+        // This means we should be animating the PiP activity leash, since we do the reparenting
+        // of the PiP activity back to its original task in startWCT.
+        TransitionInfo.Change pipChange = null;
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (change.getTaskInfo() == null
+                    && change.getLastParent() != null
+                    && change.getLastParent().equals(pipToken)) {
+                pipChange = change;
+                break;
+            }
+        }
+        // failsafe
+        if (pipChange == null || pipChange.getLeash() == null) {
+            return false;
+        }
+        mFinishCallback = finishCallback;
+
+        // Get the original parent before PiP. If original task hosting the PiP activity was
+        // already visible, then it's not participating in this transition; in that case,
+        // parentBeforePip would be null.
+        final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent());
+
+        final Rect startBounds = pipChange.getStartAbsBounds();
+        final Rect endBounds = pipChange.getEndAbsBounds();
+        if (parentBeforePip != null) {
+            // Since we have the parent task amongst the targets, all PiP activity
+            // leash translations will be relative to the original task, NOT the root leash.
+            startBounds.offset(-parentBeforePip.getStartAbsBounds().left,
+                    -parentBeforePip.getStartAbsBounds().top);
+            endBounds.offset(-parentBeforePip.getEndAbsBounds().left,
+                    -parentBeforePip.getEndAbsBounds().top);
+        }
+
+        final SurfaceControl pipLeash = pipChange.getLeash();
+        PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+                startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+                null /* srcRectHint */, ROTATION_0 /* delta */);
+
+
+        mSplitScreenControllerOptional.ifPresent(splitController -> {
+            splitController.finishEnterSplitScreen(finishTransaction);
+        });
+
+        animator.setAnimationEndCallback(() -> {
+            if (parentBeforePip == null) {
+                // After PipExpandAnimator is done modifying finishTransaction, we need to make
+                // sure PiP activity leash is offset at origin relative to its task as we reparent
+                // targets back from the transition root leash.
+                finishTransaction.setPosition(pipLeash, 0, 0);
+            }
+            finishTransition();
+        });
+        cacheAndStartTransitionAnimator(animator);
+        saveReentryState();
+        return true;
+    }
+
+    private void finishTransition() {
+        final int currentState = mPipTransitionState.getState();
+        if (currentState != PipTransitionState.EXITING_PIP) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "Unexpected state %s as we are finishing an exit-via-expand transition",
+                    mPipTransitionState);
+        }
+        mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+
+        if (mFinishCallback != null) {
+            // Need to unset mFinishCallback first because onTransitionFinished can re-enter this
+            // handler if there is a pending PiP animation.
+            final Transitions.TransitionFinishCallback finishCallback = mFinishCallback;
+            mFinishCallback = null;
+            finishCallback.onTransitionFinished(null /* finishWct */);
+        }
+    }
+
+    private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
+        final Rect endBounds = outPipTaskChange.getEndAbsBounds();
+        final int width = endBounds.width();
+        final int height = endBounds.height();
+        final int left = endBounds.left;
+        final int top = endBounds.top;
+        int newTop, newLeft;
+
+        if (delta == Surface.ROTATION_90) {
+            newLeft = top;
+            newTop = -(left + width);
+        } else {
+            newLeft = -(height + top);
+            newTop = left;
+        }
+        // Modify the endBounds, rotating and placing them potentially off-screen, so that
+        // as we translate and rotate around the origin, we place them right into the target.
+        endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
+    }
+
+    private void saveReentryState() {
+        float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+                mPipBoundsState.getBounds());
+        mPipBoundsState.saveReentryState(snapFraction);
+    }
+
+    private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) {
+        mTransitionAnimator = animator;
+        mTransitionAnimator.start();
+    }
+
+    @VisibleForTesting
+    interface PipExpandAnimatorSupplier {
+        PipExpandAnimator get(Context context,
+                @NonNull SurfaceControl leash,
+                SurfaceControl.Transaction startTransaction,
+                SurfaceControl.Transaction finishTransaction,
+                @NonNull Rect baseBounds,
+                @NonNull Rect startBounds,
+                @NonNull Rect endBounds,
+                @Nullable Rect sourceRectHint,
+                @Surface.Rotation int rotation);
+    }
+
+    @VisibleForTesting
+    void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) {
+        mPipExpandAnimatorSupplier = supplier;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
new file mode 100644
index 0000000..01cda6c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2025 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.pip2.phone.transition;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_0;
+
+import android.annotation.NonNull;
+import android.app.PictureInPictureParams;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+
+/**
+ * A set of utility methods to help resolve PiP transitions.
+ */
+public class PipTransitionUtils {
+
+    /**
+     * @return change for a pinned mode task; null if no such task is in the list of changes.
+     */
+    @Nullable
+    public static TransitionInfo.Change getPipChange(TransitionInfo info) {
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (change.getTaskInfo() != null
+                    && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+                return change;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return change for a task with the provided token; null if no task with such token found.
+     */
+    @Nullable
+    public static TransitionInfo.Change getChangeByToken(TransitionInfo info,
+            WindowContainerToken token) {
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (change.getTaskInfo() != null
+                    && change.getTaskInfo().getToken().equals(token)) {
+                return change;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the leash to interact with the container this change represents.
+     * @throws NullPointerException if the leash is null.
+     */
+    @NonNull
+    public static SurfaceControl getLeash(TransitionInfo.Change change) {
+        SurfaceControl leash = change.getLeash();
+        Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
+        return leash;
+    }
+
+    /**
+     * Get the rotation delta in a potential fixed rotation transition.
+     *
+     * Whenever PiP participates in fixed rotation, its actual orientation isn't updated
+     * in the initial transition as per the async rotation convention.
+     *
+     * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already.
+     * @param pipDisplayLayoutState display layout state that PiP component keeps track of.
+     */
+    @Surface.Rotation
+    public static int getFixedRotationDelta(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change pipChange,
+            @NonNull PipDisplayLayoutState pipDisplayLayoutState) {
+        TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+        int startRotation = pipChange.getStartRotation();
+        if (pipChange.getEndRotation() != ROTATION_UNDEFINED
+                && startRotation != pipChange.getEndRotation()) {
+            // If PiP change was collected along with the display change and the orientation change
+            // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
+            return ROTATION_0;
+        }
+
+        int endRotation = fixedRotationChange != null
+                ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation();
+        int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
+                : startRotation - endRotation;
+        return delta;
+    }
+
+    /**
+     * Gets a change amongst the transition targets that is in a different final orientation than
+     * the display, signalling a potential fixed rotation transition.
+     */
+    @Nullable
+    public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
+                return change;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return {@link PictureInPictureParams} provided by the client from the PiP change.
+     */
+    @NonNull
+    public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
+        return pipChange.getTaskInfo().pictureInPictureParams != null
+                ? pipChange.getTaskInfo().pictureInPictureParams
+                : new PictureInPictureParams.Builder().build();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 4f2e028..2fa0966 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -381,7 +381,8 @@
     private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
-                || taskInfo.realActivity == null) {
+                || taskInfo.realActivity == null
+                || excludeTaskFromGeneratedList(taskInfo)) {
             return;
         }
         try {
@@ -397,7 +398,8 @@
     private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
-                || taskInfo.realActivity == null) {
+                || taskInfo.realActivity == null
+                || excludeTaskFromGeneratedList(taskInfo)) {
             return;
         }
         try {
@@ -413,7 +415,8 @@
     private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
-                || taskInfo.realActivity == null) {
+                || taskInfo.realActivity == null
+                || excludeTaskFromGeneratedList(taskInfo)) {
             return;
         }
         try {
@@ -430,7 +433,8 @@
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
                 || taskInfo.realActivity == null
-                || enableShellTopTaskTracking()) {
+                || enableShellTopTaskTracking()
+                || excludeTaskFromGeneratedList(taskInfo)) {
             return;
         }
         try {
@@ -447,7 +451,8 @@
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
                 || taskInfo.realActivity == null
-                || enableShellTopTaskTracking()) {
+                || enableShellTopTaskTracking()
+                || excludeTaskFromGeneratedList(taskInfo)) {
             return;
         }
         try {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index a969845..847a038 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -796,7 +796,8 @@
                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                                 "  unhandled root taskId=%d", taskInfo.taskId);
                     }
-                } else if (TransitionUtil.isDividerBar(change)) {
+                } else if (TransitionUtil.isDividerBar(change)
+                        || TransitionUtil.isDimLayer(change)) {
                     final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
                             belowLayers - i, info, t, mLeashMap);
                     // Add this as a app and we will separate them on launcher side by window type.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index a799b7f..73b42d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -40,11 +40,13 @@
 import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit;
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
+import static com.android.wm.shell.common.split.SplitLayout.RESTING_DIM_LAYER;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
 import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
 import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIM_LAYER;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
@@ -1824,6 +1826,14 @@
         // Ensure divider surface are re-parented back into the hierarchy at the end of the
         // transition. See Transition#buildFinishTransaction for more detail.
         finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
+        if (Flags.enableFlexibleSplit()) {
+            mStageOrderOperator.getActiveStages().forEach(stage -> {
+                finishT.reparent(stage.mDimLayer, stage.mRootLeash);
+            });
+        } else if (Flags.enableFlexibleTwoAppSplit()) {
+            finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash);
+            finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash);
+        }
 
         updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
         finishT.show(mRootTaskLeash);
@@ -3540,6 +3550,9 @@
 
         finishEnterSplitScreen(finishT);
         addDividerBarToTransition(info, true /* show */);
+        if (Flags.enableFlexibleTwoAppSplit()) {
+            addAllDimLayersToTransition(info, true /* show */);
+        }
         return true;
     }
 
@@ -3790,6 +3803,9 @@
         }
 
         addDividerBarToTransition(info, false /* show */);
+        if (Flags.enableFlexibleTwoAppSplit()) {
+            addAllDimLayersToTransition(info, false /* show */);
+        }
     }
 
     /** Call this when the recents animation canceled during split-screen. */
@@ -3836,6 +3852,19 @@
                 returnToApp);
         mPausingTasks.clear();
         if (returnToApp) {
+            // Reparent auxiliary surfaces (divider bar and dim layers) back onto their
+            // original roots.
+            if (Flags.enableFlexibleSplit()) {
+                mStageOrderOperator.getActiveStages().forEach(stage -> {
+                    finishT.reparent(stage.mDimLayer, stage.mRootLeash);
+                    finishT.setLayer(stage.mDimLayer, RESTING_DIM_LAYER);
+                });
+            } else if (Flags.enableFlexibleTwoAppSplit()) {
+                finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash);
+                finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash);
+                finishT.setLayer(mMainStage.mDimLayer, RESTING_DIM_LAYER);
+                finishT.setLayer(mSideStage.mDimLayer, RESTING_DIM_LAYER);
+            }
             updateSurfaceBounds(mSplitLayout, finishT,
                     false /* applyResizingOffset */);
             finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
@@ -3902,6 +3931,39 @@
         info.addChange(barChange);
     }
 
+    /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */
+    private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) {
+        if (Flags.enableFlexibleSplit()) {
+            List<StageTaskListener> stages = mStageOrderOperator.getActiveStages();
+            for (int i = 0; i < stages.size(); i++) {
+                final StageTaskListener stage = stages.get(i);
+                mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1);
+                addDimLayerToTransition(info, show, stage, mTempRect1);
+            }
+        } else {
+            addDimLayerToTransition(info, show, mMainStage, getMainStageBounds());
+            addDimLayerToTransition(info, show, mSideStage, getSideStageBounds());
+        }
+    }
+
+    /** Adds a single dim layer to the given TransitionInfo. */
+    private void addDimLayerToTransition(@NonNull TransitionInfo info, boolean show,
+            StageTaskListener stage, Rect bounds) {
+        final SurfaceControl dimLayer = stage.mDimLayer;
+        if (dimLayer == null || !dimLayer.isValid()) {
+            Slog.w(TAG, "addDimLayerToTransition but leash was released or not created");
+        } else {
+            final TransitionInfo.Change change =
+                    new TransitionInfo.Change(null /* token */, dimLayer);
+            change.setParent(mRootTaskInfo.token);
+            change.setStartAbsBounds(bounds);
+            change.setEndAbsBounds(bounds);
+            change.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
+            change.setFlags(FLAG_IS_DIM_LAYER);
+            info.addChange(change);
+        }
+    }
+
     @NeverCompile
     @Override
     public void dump(@NonNull PrintWriter pw, String prefix) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index dd5439a..7871179 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -339,6 +339,7 @@
                         taskInfo,
                         taskSurface,
                         mMainHandler,
+                        mMainExecutor,
                         mBgExecutor,
                         mMainChoreographer,
                         mSyncQueue,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 23bb2aa..49510c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -48,6 +48,8 @@
 import android.view.ViewConfiguration;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.window.DesktopModeFlags;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -58,6 +60,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
@@ -69,6 +72,7 @@
  */
 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
     private final Handler mHandler;
+    private final @ShellMainThread ShellExecutor mMainExecutor;
     private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
@@ -90,6 +94,7 @@
             RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             Handler handler,
+            @ShellMainThread ShellExecutor mainExecutor,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
@@ -97,6 +102,7 @@
         super(context, userContext, displayController, taskOrganizer, taskInfo,
                 taskSurface, windowDecorViewHostSupplier);
         mHandler = handler;
+        mMainExecutor = mainExecutor;
         mBgExecutor = bgExecutor;
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
@@ -287,8 +293,14 @@
 
         if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
             closeDragResizeListener();
+            final ShellExecutor bgExecutor =
+                    DesktopModeFlags.ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD.isTrue()
+                            ? mBgExecutor : mMainExecutor;
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    WindowManagerGlobal.getWindowSession(),
+                    mMainExecutor,
+                    bgExecutor,
                     mTaskInfo,
                     mHandler,
                     mChoreographer,
@@ -299,17 +311,19 @@
                     mSurfaceControlTransactionSupplier,
                     mDisplayController);
         }
-
+        final DragResizeInputListener newListener = mDragResizeListener;
         final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
                 .getScaledTouchSlop();
-
         final Resources res = mResult.mRootView.getResources();
-        mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */,
-                        new Size(mResult.mWidth, mResult.mHeight),
-                        getResizeEdgeHandleSize(res),
-                        getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
-                        getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE),
-                touchSlop);
+        final DragResizeWindowGeometry newGeometry = new DragResizeWindowGeometry(
+                0 /* taskCornerRadius */,
+                new Size(mResult.mWidth, mResult.mHeight),
+                getResizeEdgeHandleSize(res),
+                getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
+                getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE);
+        newListener.addInitializedCallback(() -> {
+            mDragResizeListener.setGeometry(newGeometry, touchSlop);
+        });
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1cc04b4..5a6ea21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -149,6 +149,8 @@
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
 import com.android.wm.shell.windowdecor.extension.InsetsStateKt;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
 
 import kotlin.Pair;
@@ -173,7 +175,7 @@
  */
 
 public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
-        FocusTransitionListener {
+        FocusTransitionListener, SnapEventHandler {
     private static final String TAG = "DesktopModeWindowDecorViewModel";
 
     private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@@ -255,6 +257,7 @@
     private final WindowDecorTaskResourceLoader mTaskResourceLoader;
     private final RecentsTransitionHandler mRecentsTransitionHandler;
     private final DesktopModeCompatPolicy mDesktopModeCompatPolicy;
+    private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -292,7 +295,8 @@
             DesktopModeUiEventLogger desktopModeUiEventLogger,
             WindowDecorTaskResourceLoader taskResourceLoader,
             RecentsTransitionHandler recentsTransitionHandler,
-            DesktopModeCompatPolicy desktopModeCompatPolicy) {
+            DesktopModeCompatPolicy desktopModeCompatPolicy,
+            DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
         this(
                 context,
                 shellExecutor,
@@ -335,7 +339,8 @@
                 desktopModeUiEventLogger,
                 taskResourceLoader,
                 recentsTransitionHandler,
-                desktopModeCompatPolicy);
+                desktopModeCompatPolicy,
+                desktopTilingDecorViewModel);
     }
 
     @VisibleForTesting
@@ -381,7 +386,8 @@
             DesktopModeUiEventLogger desktopModeUiEventLogger,
             WindowDecorTaskResourceLoader taskResourceLoader,
             RecentsTransitionHandler recentsTransitionHandler,
-            DesktopModeCompatPolicy desktopModeCompatPolicy) {
+            DesktopModeCompatPolicy desktopModeCompatPolicy,
+            DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
@@ -452,7 +458,8 @@
         mTaskResourceLoader = taskResourceLoader;
         mRecentsTransitionHandler = recentsTransitionHandler;
         mDesktopModeCompatPolicy = desktopModeCompatPolicy;
-
+        mDesktopTilingDecorViewModel = desktopTilingDecorViewModel;
+        mDesktopTasksController.setSnapEventHandler(this);
         shellInit.addInitCallback(this::onInit, this);
     }
 
@@ -723,8 +730,7 @@
                 decoration.mTaskInfo,
                 left ? SnapPosition.LEFT : SnapPosition.RIGHT,
                 left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU,
-                inputMethod,
-                decoration);
+                inputMethod);
 
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
@@ -885,6 +891,33 @@
         return snapshotList;
     }
 
+    @Override
+    public boolean snapToHalfScreen(@NonNull RunningTaskInfo taskInfo,
+            @NonNull Rect currentDragBounds, @NonNull SnapPosition position) {
+        return mDesktopTilingDecorViewModel.snapToHalfScreen(taskInfo,
+                mWindowDecorByTaskId.get(taskInfo.taskId), position, currentDragBounds);
+    }
+
+    @Override
+    public void removeTaskIfTiled(int displayId, int taskId) {
+        mDesktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId);
+    }
+
+    @Override
+    public void onUserChange() {
+        mDesktopTilingDecorViewModel.onUserChange();
+    }
+
+    @Override
+    public void onOverviewAnimationStateChange(boolean running) {
+        mDesktopTilingDecorViewModel.onOverviewAnimationStateChange(running);
+    }
+
+    @Override
+    public boolean moveTaskToFrontIfTiled(@NonNull RunningTaskInfo taskInfo) {
+        return mDesktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo);
+    }
+
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
@@ -951,7 +984,7 @@
                             mDesktopTasksController.onDesktopWindowClose(
                                     wct, mDisplayId, decoration.mTaskInfo);
                     final IBinder transition = mTaskOperations.closeTask(mTaskToken, wct);
-                    if (transition != null && runOnTransitionStart != null) {
+                    if (transition != null) {
                         runOnTransitionStart.invoke(transition);
                     }
                 }
@@ -1238,8 +1271,7 @@
                             taskInfo, decoration.mTaskSurface,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                             newTaskBounds, decoration.calculateValidDragArea(),
-                            new Rect(mOnDragStartInitialBounds), e,
-                            mWindowDecorByTaskId.get(taskInfo.taskId));
+                            new Rect(mOnDragStartInitialBounds), e);
                     if (touchingButton) {
                         // We need the input event to not be consumed here to end the ripple
                         // effect on the touched button. We will reset drag state in the ensuing
@@ -1444,16 +1476,13 @@
                         relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
                 boolean dragFromStatusBarAllowed = false;
                 final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
-                if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
+                if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                        || BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
                     // In proto2 any full screen or multi-window task can be dragged to
                     // freeform.
                     dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
                             || windowingMode == WINDOWING_MODE_MULTI_WINDOW;
                 }
-                if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
-                    // TODO(b/388851898): add support for split screen (multi-window wm mode)
-                    dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN;
-                }
                 final boolean shouldStartTransitionDrag =
                         relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
                                 || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue();
@@ -1502,7 +1531,11 @@
                     // Do not create an indicator at all if we're not past transition height.
                     DisplayLayout layout = mDisplayController
                             .getDisplayLayout(relevantDecor.mTaskInfo.displayId);
-                    if (ev.getRawY() < 2 * layout.stableInsets().top
+                    // It's possible task is not at the top of the screen (e.g. bottom of vertical
+                    // Splitscreen)
+                    final int taskTop = relevantDecor.mTaskInfo.configuration.windowConfiguration
+                            .getBounds().top;
+                    if (ev.getRawY() < 2 * layout.stableInsets().top + taskTop
                             && mMoveToDesktopAnimator == null) {
                         return;
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 3fb9463..dca376f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -68,6 +68,7 @@
 import android.view.ViewConfiguration;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
 import android.widget.ImageButton;
 import android.window.DesktopModeFlags;
 import android.window.TaskSnapshot;
@@ -479,7 +480,7 @@
         if (shouldDelayUpdate) {
             return;
         }
-        updateDragResizeListener(mDecorationContainerSurface, inFullImmersive);
+        updateDragResizeListenerIfNeeded(mDecorationContainerSurface, inFullImmersive);
     }
 
 
@@ -587,7 +588,7 @@
             closeMaximizeMenu();
             notifyNoCaptionHandle();
         }
-        updateDragResizeListener(oldDecorationSurface, inFullImmersive);
+        updateDragResizeListenerIfNeeded(oldDecorationSurface, inFullImmersive);
         updateMaximizeMenu(startT, inFullImmersive);
         Trace.endSection(); // DesktopModeWindowDecoration#relayout
     }
@@ -665,22 +666,42 @@
         return mUserContext.getUser();
     }
 
-    private void updateDragResizeListener(SurfaceControl oldDecorationSurface,
+    private void updateDragResizeListenerIfNeeded(@Nullable SurfaceControl containerSurface,
             boolean inFullImmersive) {
+        final boolean taskPositionChanged = !mTaskInfo.positionInParent.equals(mPositionInParent);
         if (!isDragResizable(mTaskInfo, inFullImmersive)) {
-            if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
+            if (taskPositionChanged) {
                 // We still want to track caption bar's exclusion region on a non-resizeable task.
                 updateExclusionRegion(inFullImmersive);
             }
             closeDragResizeListener();
             return;
         }
+        updateDragResizeListener(containerSurface,
+                (geometryChanged) -> {
+                    if (geometryChanged || taskPositionChanged) {
+                        updateExclusionRegion(inFullImmersive);
+                    }
+                });
+    }
 
-        if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
+    private void updateDragResizeListener(@Nullable SurfaceControl containerSurface,
+            Consumer<Boolean> onUpdateFinished) {
+        final boolean containerSurfaceChanged = containerSurface != mDecorationContainerSurface;
+        final boolean isFirstDragResizeListener = mDragResizeListener == null;
+        final boolean shouldCreateListener = containerSurfaceChanged || isFirstDragResizeListener;
+        if (containerSurfaceChanged) {
             closeDragResizeListener();
-            Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener");
+        }
+        if (shouldCreateListener) {
+            final ShellExecutor bgExecutor =
+                    DesktopModeFlags.ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD.isTrue()
+                            ? mBgExecutor : mMainExecutor;
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    WindowManagerGlobal.getWindowSession(),
+                    mMainExecutor,
+                    bgExecutor,
                     mTaskInfo,
                     mHandler,
                     mChoreographer,
@@ -691,24 +712,20 @@
                     mSurfaceControlTransactionSupplier,
                     mDisplayController,
                     mDesktopModeEventLogger);
-            Trace.endSection();
         }
-
+        final DragResizeInputListener newListener = mDragResizeListener;
         final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
                 .getScaledTouchSlop();
-
-        // If either task geometry or position have changed, update this task's
-        // exclusion region listener
         final Resources res = mResult.mRootView.getResources();
-        if (mDragResizeListener.setGeometry(
-                new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius,
-                        new Size(mResult.mWidth, mResult.mHeight),
-                        getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
-                        getFineResizeCornerSize(res), getLargeResizeCornerSize(res),
-                        mDisabledResizingEdge), touchSlop)
-                || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
-            updateExclusionRegion(inFullImmersive);
-        }
+        final DragResizeWindowGeometry newGeometry = new DragResizeWindowGeometry(
+                mRelayoutParams.mCornerRadius,
+                new Size(mResult.mWidth, mResult.mHeight),
+                getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
+                getFineResizeCornerSize(res), getLargeResizeCornerSize(res),
+                mDisabledResizingEdge);
+        newListener.addInitializedCallback(() -> {
+            onUpdateFinished.accept(newListener.setGeometry(newGeometry, touchSlop));
+        });
     }
 
     private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
@@ -803,8 +820,7 @@
         if (!mTaskInfo.isVisible()) {
             closeMaximizeMenu();
         } else {
-            final int menuWidth = calculateMaximizeMenuWidth();
-            mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT);
+            mMaximizeMenu.positionMenu(startT);
         }
     }
 
@@ -1069,27 +1085,7 @@
         return Resources.ID_NULL;
     }
 
-    private int calculateMaximizeMenuWidth() {
-        final boolean showImmersive = DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
-                && TaskInfoKt.getRequestingImmersive(mTaskInfo);
-        final boolean showMaximize = true;
-        final boolean showSnaps = mTaskInfo.isResizeable;
-        int showCount = 0;
-        if (showImmersive) showCount++;
-        if (showMaximize) showCount++;
-        if (showSnaps) showCount++;
-        return switch (showCount) {
-            case 1 -> loadDimensionPixelSize(mContext.getResources(),
-                    R.dimen.desktop_mode_maximize_menu_width_one_options);
-            case 2 -> loadDimensionPixelSize(mContext.getResources(),
-                    R.dimen.desktop_mode_maximize_menu_width_two_options);
-            case 3 -> loadDimensionPixelSize(mContext.getResources(),
-                    R.dimen.desktop_mode_maximize_menu_width_three_options);
-            default -> throw new IllegalArgumentException("");
-        };
-    }
-
-    private PointF calculateMaximizeMenuPosition(int menuWidth) {
+    private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
         final PointF position = new PointF();
         final Resources resources = mContext.getResources();
         final DisplayLayout displayLayout =
@@ -1105,9 +1101,6 @@
         final int[] maximizeButtonLocation = new int[2];
         maximizeWindowButton.getLocationInWindow(maximizeButtonLocation);
 
-        final int menuHeight = loadDimensionPixelSize(
-                resources, R.dimen.desktop_mode_maximize_menu_height);
-
         float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth
                 - maximizeWindowButton.getWidth()) / 2));
         float menuTop = (mPositionInParent.y + captionHeight);
@@ -1294,17 +1287,16 @@
      * Create and display maximize menu window
      */
     void createMaximizeMenu() {
-        final int menuWidth = calculateMaximizeMenuWidth();
         mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer,
                 mDisplayController, mTaskInfo, mContext,
-                calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier);
+                (width, height) -> calculateMaximizeMenuPosition(width, height),
+                mSurfaceControlTransactionSupplier);
 
         mMaximizeMenu.show(
                 /* isTaskInImmersiveMode= */
                 DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
                         && mDesktopUserRepositories.getProfile(mTaskInfo.userId)
                             .isTaskInFullImmersiveState(mTaskInfo.taskId),
-                /* menuWidth= */ menuWidth,
                 /* showImmersiveOption= */
                 DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
                         && TaskInfoKt.getRequestingImmersive(mTaskInfo),
@@ -1736,7 +1728,8 @@
      */
     private Region getGlobalExclusionRegion(boolean inFullImmersive) {
         Region exclusionRegion;
-        if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) {
+        if (mDragResizeListener != null
+                && isDragResizable(mTaskInfo, inFullImmersive)) {
             exclusionRegion = mDragResizeListener.getCornersRegion();
         } else {
             exclusionRegion = new Region();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index b531079..7a4a834 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -43,6 +43,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.Trace;
 import android.util.Size;
 import android.view.Choreographer;
 import android.view.IWindowSession;
@@ -54,14 +55,19 @@
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.WindowManagerGlobal;
 import android.window.InputTransferToken;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
@@ -73,28 +79,45 @@
  */
 class DragResizeInputListener implements AutoCloseable {
     private static final String TAG = "DragResizeInputListener";
-    private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
+    private final IWindowSession mWindowSession;
+    private final TaskResizeInputEventReceiverFactory mEventReceiverFactory;
+    private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
     private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
 
     private final int mDisplayId;
 
-    private final IBinder mClientToken;
+    @VisibleForTesting
+    final IBinder mClientToken;
 
     private final SurfaceControl mDecorationSurface;
-    private final InputChannel mInputChannel;
-    private final TaskResizeInputEventReceiver mInputEventReceiver;
+    private InputChannel mInputChannel;
+    private TaskResizeInputEventReceiver mInputEventReceiver;
 
     private final Context mContext;
+    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final RunningTaskInfo mTaskInfo;
-    private final SurfaceControl mInputSinkSurface;
-    private final IBinder mSinkClientToken;
-    private final InputChannel mSinkInputChannel;
+    private final Handler mHandler;
+    private final Choreographer mChoreographer;
+    private SurfaceControl mInputSinkSurface;
+    @VisibleForTesting
+    final IBinder mSinkClientToken;
+    private InputChannel mSinkInputChannel;
     private final DisplayController mDisplayController;
+    /** TODO: b/396490344 - this desktop-specific class should be abstracted out of here. */
     private final DesktopModeEventLogger mDesktopModeEventLogger;
+    private final DragPositioningCallback mDragPositioningCallback;
     private final Region mTouchRegion = new Region();
+    private final List<Runnable> mOnInitializedCallbacks = new ArrayList<>();
+
+    private final Runnable mInitInputChannels;
+    private boolean mClosed = false;
 
     DragResizeInputListener(
             Context context,
+            IWindowSession windowSession,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
+            TaskResizeInputEventReceiverFactory eventReceiverFactory,
             RunningTaskInfo taskInfo,
             Handler handler,
             Choreographer choreographer,
@@ -106,74 +129,93 @@
             DisplayController displayController,
             DesktopModeEventLogger desktopModeEventLogger) {
         mContext = context;
+        mWindowSession = windowSession;
+        mBgExecutor = bgExecutor;
+        mEventReceiverFactory = eventReceiverFactory;
         mTaskInfo = taskInfo;
-        mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
+        mHandler = handler;
+        mChoreographer = choreographer;
         mDisplayId = displayId;
         mDecorationSurface = decorationSurface;
+        mDragPositioningCallback = callback;
+        mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
+        mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mDisplayController = displayController;
         mDesktopModeEventLogger = desktopModeEventLogger;
         mClientToken = new Binder();
-        final InputTransferToken inputTransferToken = new InputTransferToken();
-        mInputChannel = new InputChannel();
-        try {
-            mWindowSession.grantInputChannel(
-                    mDisplayId,
-                    mDecorationSurface,
-                    mClientToken,
-                    null /* hostInputToken */,
-                    FLAG_NOT_FOCUSABLE,
-                    PRIVATE_FLAG_TRUSTED_OVERLAY,
-                    INPUT_FEATURE_SPY,
-                    TYPE_APPLICATION,
-                    null /* windowToken */,
-                    inputTransferToken,
-                    TAG + " of " + decorationSurface.toString(),
-                    mInputChannel);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-        }
-
-        mInputEventReceiver = new TaskResizeInputEventReceiver(context, mTaskInfo, mInputChannel,
-                callback,
-                handler, choreographer, () -> {
-            final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
-            return new Size(layout.width(), layout.height());
-        }, this::updateSinkInputChannel, mDesktopModeEventLogger);
-        mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
-
-        mInputSinkSurface = surfaceControlBuilderSupplier.get()
-                .setName("TaskInputSink of " + decorationSurface)
-                .setContainerLayer()
-                .setParent(mDecorationSurface)
-                .setCallsite("DragResizeInputListener.constructor")
-                .build();
-        mSurfaceControlTransactionSupplier.get()
-                .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER)
-                .show(mInputSinkSurface)
-                .apply();
         mSinkClientToken = new Binder();
-        mSinkInputChannel = new InputChannel();
-        try {
-            mWindowSession.grantInputChannel(
-                    mDisplayId,
-                    mInputSinkSurface,
-                    mSinkClientToken,
-                    null /* hostInputToken */,
-                    FLAG_NOT_FOCUSABLE,
-                    0 /* privateFlags */,
-                    INPUT_FEATURE_NO_INPUT_CHANNEL,
-                    TYPE_INPUT_CONSUMER,
-                    null /* windowToken */,
-                    inputTransferToken,
-                    "TaskInputSink of " + decorationSurface,
-                    mSinkInputChannel);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-        }
+
+        // Setting up input channels for both the resize listener and the input sink requires
+        // multiple blocking binder calls, so it's moved to a bg thread to keep the shell.main
+        // thread free.
+        // The input event receiver must be created back in the shell.main thread though because
+        // its geometry and util methods are updated/queried from the shell.main thread.
+        mInitInputChannels = () -> {
+            final InputSetUpResult result = setUpInputChannels(mDisplayId, mWindowSession,
+                    mDecorationSurface, mClientToken, mSinkClientToken,
+                    mSurfaceControlBuilderSupplier,
+                    mSurfaceControlTransactionSupplier);
+            mainExecutor.execute(() -> {
+                if (mClosed) {
+                    return;
+                }
+                mInputSinkSurface = result.mInputSinkSurface;
+                mInputChannel = result.mInputChannel;
+                mSinkInputChannel = result.mSinkInputChannel;
+                Trace.beginSection("DragResizeInputListener#ctor-initReceiver");
+                mInputEventReceiver = mEventReceiverFactory.create(
+                        mContext,
+                        mTaskInfo,
+                        mInputChannel,
+                        mDragPositioningCallback,
+                        mHandler,
+                        mChoreographer,
+                        () -> {
+                            final DisplayLayout layout =
+                                    mDisplayController.getDisplayLayout(mDisplayId);
+                            return new Size(layout.width(), layout.height());
+                        },
+                        this::updateSinkInputChannel,
+                        mDesktopModeEventLogger);
+                mInputEventReceiver.setTouchSlop(
+                        ViewConfiguration.get(mContext).getScaledTouchSlop());
+                for (Runnable initCallback : mOnInitializedCallbacks) {
+                    initCallback.run();
+                }
+                mOnInitializedCallbacks.clear();
+                Trace.endSection();
+            });
+        };
+        bgExecutor.execute(mInitInputChannels);
     }
 
     DragResizeInputListener(
             Context context,
+            IWindowSession windowSession,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
+            RunningTaskInfo taskInfo,
+            Handler handler,
+            Choreographer choreographer,
+            int displayId,
+            SurfaceControl decorationSurface,
+            DragPositioningCallback callback,
+            Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+            Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+            DisplayController displayController,
+            DesktopModeEventLogger desktopModeEventLogger) {
+        this(context, windowSession, mainExecutor, bgExecutor,
+                new DefaultTaskResizeInputEventReceiverFactory(), taskInfo,
+                handler, choreographer, displayId, decorationSurface, callback,
+                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
+                displayController, desktopModeEventLogger);
+    }
+
+    DragResizeInputListener(
+            Context context,
+            IWindowSession windowSession,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             RunningTaskInfo taskInfo,
             Handler handler,
             Choreographer choreographer,
@@ -183,9 +225,84 @@
             Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
             DisplayController displayController) {
-        this(context, taskInfo, handler, choreographer, displayId, decorationSurface, callback,
-                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, displayController,
-                new DesktopModeEventLogger());
+        this(context, windowSession, mainExecutor, bgExecutor, taskInfo,
+                handler, choreographer, displayId, decorationSurface, callback,
+                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
+                displayController, new DesktopModeEventLogger());
+    }
+
+    /**
+     * Registers a callback to be invoked when the input listener has finished initializing. If
+     * already finished, the callback will be invoked immediately.
+     */
+    void addInitializedCallback(Runnable onReady) {
+        if (mInputEventReceiver != null) {
+            onReady.run();
+            return;
+        }
+        mOnInitializedCallbacks.add(onReady);
+    }
+
+    @ShellBackgroundThread
+    private static InputSetUpResult setUpInputChannels(
+            int displayId,
+            @NonNull IWindowSession windowSession,
+            @NonNull SurfaceControl decorationSurface,
+            @NonNull IBinder clientToken,
+            @NonNull IBinder sinkClientToken,
+            @NonNull Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+            @NonNull Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) {
+        Trace.beginSection("DragResizeInputListener#setUpInputChannels");
+        final InputTransferToken inputTransferToken = new InputTransferToken();
+        final InputChannel inputChannel = new InputChannel();
+        final InputChannel sinkInputChannel = new InputChannel();
+        try {
+            windowSession.grantInputChannel(
+                    displayId,
+                    decorationSurface,
+                    clientToken,
+                    null /* hostInputToken */,
+                    FLAG_NOT_FOCUSABLE,
+                    PRIVATE_FLAG_TRUSTED_OVERLAY,
+                    INPUT_FEATURE_SPY,
+                    TYPE_APPLICATION,
+                    null /* windowToken */,
+                    inputTransferToken,
+                    TAG + " of " + decorationSurface,
+                    inputChannel);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        final SurfaceControl inputSinkSurface = surfaceControlBuilderSupplier.get()
+                .setName("TaskInputSink of " + decorationSurface)
+                .setContainerLayer()
+                .setParent(decorationSurface)
+                .setCallsite("DragResizeInputListener.setUpInputChannels")
+                .build();
+        surfaceControlTransactionSupplier.get()
+                .setLayer(inputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER)
+                .show(inputSinkSurface)
+                .apply();
+        try {
+            windowSession.grantInputChannel(
+                    displayId,
+                    inputSinkSurface,
+                    sinkClientToken,
+                    null /* hostInputToken */,
+                    FLAG_NOT_FOCUSABLE,
+                    0 /* privateFlags */,
+                    INPUT_FEATURE_NO_INPUT_CHANNEL,
+                    TYPE_INPUT_CONSUMER,
+                    null /* windowToken */,
+                    inputTransferToken,
+                    "TaskInputSink of " + decorationSurface,
+                    sinkInputChannel);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        Trace.endSection();
+        return new InputSetUpResult(inputSinkSurface, inputChannel, sinkInputChannel);
     }
 
     /**
@@ -268,35 +385,101 @@
     }
 
     boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
-        return mInputEventReceiver.shouldHandleEvent(e, offset);
+        return mInputEventReceiver != null && mInputEventReceiver.shouldHandleEvent(e, offset);
     }
 
     boolean isHandlingDragResize() {
-        return mInputEventReceiver.isHandlingEvents();
+        return mInputEventReceiver != null && mInputEventReceiver.isHandlingEvents();
     }
 
     @Override
     public void close() {
-        mInputEventReceiver.dispose();
-        mInputChannel.dispose();
-        try {
-            mWindowSession.remove(mClientToken);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+        mClosed = true;
+        if (mInitInputChannels != null) {
+            mBgExecutor.removeCallbacks(mInitInputChannels);
+        }
+        if (mInputEventReceiver != null) {
+            mInputEventReceiver.dispose();
+        }
+        if (mInputChannel != null) {
+            mInputChannel.dispose();
+        }
+        if (mSinkInputChannel != null) {
+            mSinkInputChannel.dispose();
         }
 
-        mSinkInputChannel.dispose();
-        try {
-            mWindowSession.remove(mSinkClientToken);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+        if (mInputSinkSurface != null) {
+            mSurfaceControlTransactionSupplier.get()
+                    .remove(mInputSinkSurface)
+                    .apply();
         }
-        mSurfaceControlTransactionSupplier.get()
-                .remove(mInputSinkSurface)
-                .apply();
+
+        mBgExecutor.execute(() -> {
+            try {
+                mWindowSession.remove(mClientToken);
+                mWindowSession.remove(mSinkClientToken);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
     }
 
-    private static class TaskResizeInputEventReceiver extends InputEventReceiver implements
+    private static class InputSetUpResult {
+        final @NonNull SurfaceControl mInputSinkSurface;
+        final @NonNull InputChannel mInputChannel;
+        final @NonNull InputChannel mSinkInputChannel;
+
+        InputSetUpResult(@NonNull SurfaceControl inputSinkSurface,
+                @NonNull InputChannel inputChannel,
+                @NonNull InputChannel sinkInputChannel) {
+            mInputSinkSurface = inputSinkSurface;
+            mInputChannel = inputChannel;
+            mSinkInputChannel = sinkInputChannel;
+        }
+    }
+
+    /** A factory that creates {@link TaskResizeInputEventReceiver}s. */
+    interface TaskResizeInputEventReceiverFactory {
+        @NonNull
+        TaskResizeInputEventReceiver create(
+                @NonNull Context context,
+                @NonNull RunningTaskInfo taskInfo,
+                @NonNull InputChannel inputChannel,
+                @NonNull DragPositioningCallback callback,
+                @NonNull Handler handler,
+                @NonNull Choreographer choreographer,
+                @NonNull Supplier<Size> displayLayoutSizeSupplier,
+                @NonNull Consumer<Region> touchRegionConsumer,
+                @NonNull DesktopModeEventLogger desktopModeEventLogger
+        );
+    }
+
+    /** A default implementation of {@link TaskResizeInputEventReceiverFactory}. */
+    static class DefaultTaskResizeInputEventReceiverFactory
+            implements TaskResizeInputEventReceiverFactory {
+        @Override
+        @NonNull
+        public TaskResizeInputEventReceiver create(
+                @NonNull Context context,
+                @NonNull RunningTaskInfo taskInfo,
+                @NonNull InputChannel inputChannel,
+                @NonNull DragPositioningCallback callback,
+                @NonNull Handler handler,
+                @NonNull Choreographer choreographer,
+                @NonNull Supplier<Size> displayLayoutSizeSupplier,
+                @NonNull Consumer<Region> touchRegionConsumer,
+                @NonNull DesktopModeEventLogger desktopModeEventLogger) {
+            return new TaskResizeInputEventReceiver(context, taskInfo, inputChannel, callback,
+                    handler, choreographer, displayLayoutSizeSupplier, touchRegionConsumer,
+                    desktopModeEventLogger);
+        }
+    }
+
+    /**
+     * An input event receiver to handle motion events on the task's corners and edges for
+     * drag-resizing, as well as keeping the input sink updated.
+     */
+    static class TaskResizeInputEventReceiver extends InputEventReceiver implements
             DragDetector.MotionEventHandler {
         @NonNull private final Context mContext;
         @NonNull private final RunningTaskInfo mTaskInfo;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 38accce..ad3525a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -90,7 +90,7 @@
         private val displayController: DisplayController,
         private val taskInfo: RunningTaskInfo,
         private val decorWindowContext: Context,
-        private val menuPosition: PointF,
+        private val positionSupplier: (Int, Int) -> PointF,
         private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
 ) {
     private var maximizeMenu: AdditionalViewHostViewContainer? = null
@@ -100,19 +100,19 @@
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
-    private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
+    private lateinit var menuPosition: PointF
     private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
 
     /** Position the menu relative to the caption's position. */
-    fun positionMenu(position: PointF, t: Transaction) {
-        menuPosition.set(position)
+    fun positionMenu(t: Transaction) {
+        menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0,
+                                        maximizeMenuView?.measureHeight() ?: 0)
         t.setPosition(leash, menuPosition.x, menuPosition.y)
     }
 
     /** Creates and shows the maximize window. */
     fun show(
         isTaskInImmersiveMode: Boolean,
-        menuWidth: Int,
         showImmersiveOption: Boolean,
         showSnapOptions: Boolean,
         onMaximizeOrRestoreClickListener: () -> Unit,
@@ -125,7 +125,6 @@
         if (maximizeMenu != null) return
         createMaximizeMenu(
             isTaskInImmersiveMode = isTaskInImmersiveMode,
-            menuWidth = menuWidth,
             showImmersiveOption = showImmersiveOption,
             showSnapOptions = showSnapOptions,
             onMaximizeClickListener = onMaximizeOrRestoreClickListener,
@@ -161,7 +160,6 @@
     /** Create a maximize menu that is attached to the display area. */
     private fun createMaximizeMenu(
         isTaskInImmersiveMode: Boolean,
-        menuWidth: Int,
         showImmersiveOption: Boolean,
         showSnapOptions: Boolean,
         onMaximizeClickListener: () -> Unit,
@@ -178,16 +176,6 @@
                 .setName("Maximize Menu")
                 .setContainerLayer()
                 .build()
-        val lp = WindowManager.LayoutParams(
-                menuWidth,
-                menuHeight,
-                WindowManager.LayoutParams.TYPE_APPLICATION,
-                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                        or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
-                PixelFormat.TRANSPARENT
-        )
-        lp.title = "Maximize Menu for Task=" + taskInfo.taskId
-        lp.setTrustedOverlay()
         val windowManager = WindowlessWindowManager(
                 taskInfo.configuration,
                 leash,
@@ -207,7 +195,6 @@
                 MaximizeMenuView.ImmersiveConfig.Hidden
             },
             showSnapOptions = showSnapOptions,
-            menuHeight = menuHeight,
             menuPadding = menuPadding,
         ).also { menuView ->
             menuView.bind(taskInfo)
@@ -217,6 +204,19 @@
             menuView.onRightSnapClickListener = onRightSnapClickListener
             menuView.onMenuHoverListener = onHoverListener
             menuView.onOutsideTouchListener = onOutsideTouchListener
+            val menuWidth = menuView.measureWidth()
+            val menuHeight = menuView.measureHeight()
+            menuPosition = positionSupplier(menuWidth, menuHeight)
+            val lp = WindowManager.LayoutParams(
+                    menuWidth.toInt(),
+                    menuHeight.toInt(),
+                    WindowManager.LayoutParams.TYPE_APPLICATION,
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+                    PixelFormat.TRANSPARENT
+            )
+            lp.title = "Maximize Menu for Task=" + taskInfo.taskId
+            lp.setTrustedOverlay()
             viewHost.setView(menuView.rootView, lp)
         }
 
@@ -268,7 +268,6 @@
         private val sizeToggleDirection: SizeToggleDirection,
         immersiveConfig: ImmersiveConfig,
         showSnapOptions: Boolean,
-        private val menuHeight: Int,
         private val menuPadding: Int
     ) {
         val rootView = LayoutInflater.from(context)
@@ -583,7 +582,7 @@
                             // the menu.
                             val value = animatedValue as Float
                             val topPadding = menuPadding -
-                                    ((1 - value) * menuHeight).toInt()
+                                    ((1 - value) * measureHeight()).toInt()
                             container.setPadding(menuPadding, topPadding,
                                 menuPadding, menuPadding)
                         }
@@ -604,7 +603,7 @@
                     }
                 },
                 ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
-                    (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+                    (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply {
                     duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
                     interpolator = EMPHASIZED_DECELERATE
                 },
@@ -667,7 +666,7 @@
                                     // the menu.
                                     val value = animatedValue as Float
                                     val topPadding = menuPadding -
-                                            ((1 - value) * menuHeight).toInt()
+                                            ((1 - value) * measureHeight()).toInt()
                                     container.setPadding(menuPadding, topPadding,
                                             menuPadding, menuPadding)
                                 }
@@ -688,7 +687,7 @@
                         }
                     },
                     ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
-                            0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply {
+                            0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply {
                         duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
                         interpolator = FAST_OUT_LINEAR_IN
                     },
@@ -792,6 +791,18 @@
             )
         }
 
+        /** Measure width of the root view of this menu. */
+        fun measureWidth() : Int {
+            rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+            return rootView.getMeasuredWidth()
+        }
+
+        /** Measure height of the root view of this menu. */
+        fun measureHeight() : Int {
+            rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+            return rootView.getMeasuredHeight()
+        }
+
         private fun deactivateSnapOptions() {
             // TODO(b/346440693): the background/colorStateList set on these buttons is overridden
             //  to a static resource & color on manually tracked hover events, which defeats the
@@ -1036,7 +1047,7 @@
         displayController: DisplayController,
         taskInfo: RunningTaskInfo,
         decorWindowContext: Context,
-        menuPosition: PointF,
+        positionSupplier: (Int, Int) -> PointF,
         transactionSupplier: Supplier<Transaction>
     ): MaximizeMenu
 }
@@ -1049,7 +1060,7 @@
         displayController: DisplayController,
         taskInfo: RunningTaskInfo,
         decorWindowContext: Context,
-        menuPosition: PointF,
+        positionSupplier: (Int, Int) -> PointF,
         transactionSupplier: Supplier<Transaction>
     ): MaximizeMenu {
         return MaximizeMenu(
@@ -1058,7 +1069,7 @@
             displayController,
             taskInfo,
             decorWindowContext,
-            menuPosition,
+            positionSupplier,
             transactionSupplier
         )
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 25dadfd..4002dc5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -361,6 +361,8 @@
         }
 
         outResult.mRootView = rootView;
+        final boolean fontScaleChanged = mWindowDecorConfig != null
+                && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale;
         final int oldDensityDpi = mWindowDecorConfig != null
                 ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
         final int oldNightMode =  mWindowDecorConfig != null
@@ -375,7 +377,8 @@
                 || mDisplay.getDisplayId() != mTaskInfo.displayId
                 || oldLayoutResId != mLayoutResId
                 || oldNightMode != newNightMode
-                || mDecorWindowContext == null) {
+                || mDecorWindowContext == null
+                || fontScaleChanged) {
             releaseViews(wct);
 
             if (!obtainDisplayOrRegisterListener()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
index 4a09614..a5592f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
@@ -50,9 +50,8 @@
     @VisibleForTesting
     val viewHostAdapter: SurfaceControlViewHostAdapter =
         SurfaceControlViewHostAdapter(context, display),
+    private val rootView: FrameLayout = FrameLayout(context)
 ) : WindowDecorViewHost, Warmable {
-    @VisibleForTesting val rootView = FrameLayout(context)
-
     private var currentUpdateJob: Job? = null
 
     override val surfaceControl: SurfaceControl
@@ -131,8 +130,10 @@
         Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost")
         viewHostAdapter.prepareViewHost(configuration, touchableRegion)
         onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) }
-        rootView.removeAllViews()
-        rootView.addView(view)
+        if (view.parent != rootView) {
+            rootView.removeAllViews()
+            rootView.addView(view)
+        }
         viewHostAdapter.updateView(rootView, attrs)
         Trace.endSection()
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt
new file mode 100644
index 0000000..52e24d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 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.windowdecor.tiling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+
+/** Interface for handling snap to half screen events. */
+interface SnapEventHandler {
+    /** Snaps an app to half the screen for tiling. */
+    fun snapToHalfScreen(
+        taskInfo: RunningTaskInfo,
+        currentDragBounds: Rect,
+        position: SnapPosition,
+    ): Boolean
+
+    /** Removes a task from tiling if it's tiled, for example on task exiting. */
+    fun removeTaskIfTiled(displayId: Int, taskId: Int)
+
+    /** Notifies the tiling handler of user switch. */
+    fun onUserChange()
+
+    /** Notifies the tiling handler of overview animation state change. */
+    fun onOverviewAnimationStateChange(running: Boolean)
+
+    /** If a task is tiled, delegate moving to front to tiling infrastructure. */
+    fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index bf5e374..bff12d0 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -45,6 +45,7 @@
         "androidx.test.rules",
         "androidx.test.ext.junit",
         "androidx.datastore_datastore",
+        "androidx.core_core-animation-testing",
         "kotlinx_coroutines_test",
         "androidx.dynamicanimation_dynamicanimation",
         "dagger2",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
index d3de0f7..3d5e949 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -16,26 +16,52 @@
 
 package com.android.wm.shell.common;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.RectF;
 import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
+import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
+import android.testing.TestableContext;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+import android.view.IDisplayWindowListener;
 import android.view.IWindowManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestSyncExecutor;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellInit;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.util.function.Consumer;
 
 /**
  * Tests for the display controller.
@@ -46,23 +72,163 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class DisplayControllerTests extends ShellTestCase {
-
-    private @Mock Context mContext;
-    private @Mock IWindowManager mWM;
-    private @Mock ShellInit mShellInit;
-    private @Mock ShellExecutor mMainExecutor;
-    private @Mock DisplayManager mDisplayManager;
+    @Mock private IWindowManager mWM;
+    @Mock private ShellInit mShellInit;
+    @Mock private DisplayManager mDisplayManager;
+    @Mock private DisplayTopology mMockTopology;
+    @Mock private DisplayController.OnDisplaysChangedListener mListener;
+    private StaticMockitoSession mMockitoSession;
+    private TestSyncExecutor mMainExecutor;
+    private IDisplayWindowListener mDisplayContainerListener;
+    private Consumer<DisplayTopology> mCapturedTopologyListener;
+    private Display mMockDisplay;
     private DisplayController mController;
+    private static final int DISPLAY_ID_0 = 0;
+    private static final int DISPLAY_ID_1 = 1;
+    private static final RectF DISPLAY_ABS_BOUNDS_0 = new RectF(10, 10, 20, 20);
+    private static final RectF DISPLAY_ABS_BOUNDS_1 = new RectF(11, 11, 22, 22);
 
     @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
+    public void setUp() throws RemoteException {
+        mMockitoSession =
+                ExtendedMockito.mockitoSession()
+                        .initMocks(this)
+                        .mockStatic(DesktopModeStatus.class)
+                        .strictness(Strictness.LENIENT)
+                        .startMocking();
+
+        mContext = spy(new TestableContext(
+                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+                        .getContext(), null));
+
+        mMainExecutor = new TestSyncExecutor();
         mController = new DisplayController(
                 mContext, mWM, mShellInit, mMainExecutor, mDisplayManager);
+
+        mMockDisplay = mock(Display.class);
+        when(mMockDisplay.getDisplayAdjustments()).thenReturn(
+                new DisplayAdjustments(new Configuration()));
+        when(mDisplayManager.getDisplay(anyInt())).thenReturn(mMockDisplay);
+        when(mDisplayManager.getDisplayTopology()).thenReturn(mMockTopology);
+        doAnswer(invocation -> {
+            mDisplayContainerListener = invocation.getArgument(0);
+            return new int[]{DISPLAY_ID_0};
+        }).when(mWM).registerDisplayWindowListener(any());
+        doAnswer(invocation -> {
+            mCapturedTopologyListener = invocation.getArgument(1);
+            return null;
+        }).when(mDisplayManager).registerTopologyListener(any(), any());
+        SparseArray<RectF> absoluteBounds = new SparseArray<>();
+        absoluteBounds.put(DISPLAY_ID_0, DISPLAY_ABS_BOUNDS_0);
+        absoluteBounds.put(DISPLAY_ID_1, DISPLAY_ABS_BOUNDS_1);
+        when(mMockTopology.getAbsoluteBounds()).thenReturn(absoluteBounds);
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
     }
 
     @Test
     public void instantiateController_addInitCallback() {
         verify(mShellInit, times(1)).addInitCallback(any(), eq(mController));
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+    public void onInit_canEnterDesktopMode_registerListeners() throws RemoteException {
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+        mController.onInit();
+
+        assertNotNull(mController.getDisplayContext(DISPLAY_ID_0));
+        verify(mWM).registerDisplayWindowListener(any());
+        verify(mDisplayManager).registerTopologyListener(eq(mMainExecutor), any());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+    public void onInit_canNotEnterDesktopMode_onlyRegisterDisplayWindowListener()
+            throws RemoteException {
+        ExtendedMockito.doReturn(false)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+        mController.onInit();
+
+        assertNotNull(mController.getDisplayContext(DISPLAY_ID_0));
+        verify(mWM).registerDisplayWindowListener(any());
+        verify(mDisplayManager, never()).registerTopologyListener(any(), any());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+    public void addDisplayWindowListener_notifiesExistingDisplaysAndTopology() {
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+        mController.onInit();
+        mController.addDisplayWindowListener(mListener);
+
+        verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0));
+        verify(mListener).onTopologyChanged(eq(mMockTopology));
+    }
+
+    @Test
+    public void onDisplayAddedAndRemoved_updatesDisplayContexts() throws RemoteException {
+        mController.onInit();
+        mController.addDisplayWindowListener(mListener);
+
+        mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+        verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0));
+        verify(mListener).onDisplayAdded(eq(DISPLAY_ID_1));
+        assertNotNull(mController.getDisplayContext(DISPLAY_ID_1));
+        verify(mContext).createDisplayContext(eq(mMockDisplay));
+
+        mDisplayContainerListener.onDisplayRemoved(DISPLAY_ID_1);
+
+        assertNull(mController.getDisplayContext(DISPLAY_ID_1));
+        verify(mListener).onDisplayRemoved(eq(DISPLAY_ID_1));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+    public void onDisplayTopologyChanged_updateDisplayLayout() throws RemoteException {
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+        mController.onInit();
+        mController.addDisplayWindowListener(mListener);
+        mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+        mCapturedTopologyListener.accept(mMockTopology);
+
+        assertEquals(DISPLAY_ABS_BOUNDS_0, mController.getDisplayLayout(DISPLAY_ID_0)
+                .globalBoundsDp());
+        assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1)
+                .globalBoundsDp());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+    public void onDisplayTopologyChanged_topologyBeforeDisplayAdded_appliesBoundsOnAdd()
+            throws RemoteException {
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+        mController.onInit();
+        mController.addDisplayWindowListener(mListener);
+
+        mCapturedTopologyListener.accept(mMockTopology);
+
+        assertNull(mController.getDisplayLayout(DISPLAY_ID_1));
+
+        mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+        assertEquals(DISPLAY_ABS_BOUNDS_0,
+                mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp());
+        assertEquals(DISPLAY_ABS_BOUNDS_1,
+                mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp());
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt
new file mode 100644
index 0000000..c91ef5e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 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
+
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [WindowContainerTransactionSupplier].
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:WindowContainerTransactionSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class WindowContainerTransactionSupplierTest {
+
+    @Test
+    fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() {
+        val supplier = WindowContainerTransactionSupplier()
+        SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+            it is WindowContainerTransaction
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java
similarity index 97%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java
index e3798e9..a6c35f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -29,9 +29,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsState;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java
similarity index 97%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java
index 85f1da5..737735c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
 
 import static org.mockito.Mockito.when;
 
@@ -27,9 +27,6 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.SizeSpecSource;
 
 import org.junit.Assert;
 import org.junit.Before;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java
similarity index 97%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java
index 080b0ae..6bda225 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -32,13 +32,6 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface;
-import com.android.wm.shell.common.pip.PipSnapAlgorithm;
-import com.android.wm.shell.common.pip.SizeSpecSource;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
similarity index 96%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
index 304da75f..ad664ac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -36,10 +36,6 @@
 import com.android.internal.util.function.TriConsumer;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.SizeSpecSource;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java
similarity index 96%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java
index b583acd..1756aad 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
 
 import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM;
 import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT;
@@ -29,8 +29,6 @@
 import android.testing.AndroidTestingRunner;
 
 import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDoubleTapHelper;
 
 import org.junit.Assert;
 import org.junit.Before;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java
similarity index 97%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java
index ac13d7f..3e71ab3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
 
 import static org.junit.Assert.assertEquals;
 
@@ -25,8 +25,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipSnapAlgorithm;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
new file mode 100644
index 0000000..22a85fc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2025 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 static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+
+import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class FlexParallaxSpecTests {
+    ParallaxSpec mFlexSpec = new FlexParallaxSpec();
+
+    Rect mDisplayBounds = new Rect(0, 0, 1000, 1000);
+    Rect mRetreatingSurface = new Rect(0, 0, 1000, 1000);
+    Rect mRetreatingContent = new Rect(0, 0, 1000, 1000);
+    Rect mAdvancingSurface = new Rect(0, 0, 1000, 1000);
+    Rect mAdvancingContent = new Rect(0, 0, 1000, 1000);
+    boolean mIsLeftRightSplit;
+    boolean mTopLeftShrink;
+
+    int mDimmingSide;
+    float mDimValue;
+    Point mRetreatingParallax = new Point(0, 0);
+    Point mAdvancingParallax = new Point(0, 0);
+
+    @Mock DividerSnapAlgorithm mockSnapAlgorithm;
+    @Mock SnapTarget mockStartEdge;
+    @Mock SnapTarget mockFirstTarget;
+    @Mock SnapTarget mockMiddleTarget;
+    @Mock SnapTarget mockLastTarget;
+    @Mock SnapTarget mockEndEdge;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mockSnapAlgorithm.getDismissStartTarget()).thenReturn(mockStartEdge);
+        when(mockSnapAlgorithm.getFirstSplitTarget()).thenReturn(mockFirstTarget);
+        when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget);
+        when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget);
+        when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge);
+
+        when(mockStartEdge.getPosition()).thenReturn(0);
+        when(mockFirstTarget.getPosition()).thenReturn(250);
+        when(mockMiddleTarget.getPosition()).thenReturn(500);
+        when(mockLastTarget.getPosition()).thenReturn(750);
+        when(mockEndEdge.getPosition()).thenReturn(1000);
+    }
+
+    @Test
+    public void testHorizontalDragFromCenter() {
+        mIsLeftRightSplit = true;
+        simulateDragFromCenterToLeft(125);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToLeft(250);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToLeft(375);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToRight(500);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+        assertThat(mDimValue).isEqualTo(0f);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToRight(625);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToRight(750);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromCenterToRight(875);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+    }
+
+    @Test
+    public void testHorizontalDragFromLeft() {
+        mIsLeftRightSplit = true;
+        simulateDragFromLeftToLeft(125);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToLeft(250);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToCenter(375);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToCenter(500);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+        assertThat(mDimValue).isEqualTo(0f);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToRight(625);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToRight(750);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromLeftToRight(875);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isGreaterThan(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+    }
+
+    @Test
+    public void testHorizontalDragFromRight() {
+        mIsLeftRightSplit = true;
+
+        simulateDragFromRightToLeft(125);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToLeft(250);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToLeft(375);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isGreaterThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToCenter(500);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+        assertThat(mDimValue).isEqualTo(0f);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToCenter(625);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(0f);
+        assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isLessThan(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToRight(750);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+        simulateDragFromRightToRight(875);
+        assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+        assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+        assertThat(mDimValue).isLessThan(1f);
+        assertThat(mRetreatingParallax.x).isEqualTo(0);
+        assertThat(mRetreatingParallax.y).isEqualTo(0);
+        assertThat(mAdvancingParallax.x).isEqualTo(0);
+        assertThat(mAdvancingParallax.y).isEqualTo(0);
+    }
+
+    private void simulateDragFromCenterToLeft(int to) {
+        int from = 500;
+
+        mRetreatingSurface = flexOffscreenAppLeft(to);
+        mRetreatingContent = onscreenAppLeft(from);
+        mAdvancingSurface = onscreenAppRight(to);
+        mAdvancingContent = onscreenAppRight(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromCenterToRight(int to) {
+        int from = 500;
+
+        mRetreatingSurface = flexOffscreenAppRight(to);
+        mRetreatingContent = onscreenAppRight(from);
+        mAdvancingSurface = onscreenAppLeft(to);
+        mAdvancingContent = onscreenAppLeft(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromLeftToLeft(int to) {
+        int from = 250;
+
+        mRetreatingSurface = flexOffscreenAppLeft(to);
+        mRetreatingContent = fullOffscreenAppLeft(from);
+        mAdvancingSurface = onscreenAppRight(to);
+        mAdvancingContent = onscreenAppRight(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromLeftToCenter(int to) {
+        int from = 250;
+
+        mRetreatingSurface = onscreenAppRight(to);
+        mRetreatingContent = onscreenAppRight(from);
+        mAdvancingSurface = fullOffscreenAppLeft(to);
+        mAdvancingContent = fullOffscreenAppLeft(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromLeftToRight(int to) {
+        int from = 250;
+
+        mRetreatingSurface = flexOffscreenAppRight(to);
+        mRetreatingContent = onscreenAppRight(from);
+        mAdvancingSurface = fullOffscreenAppLeft(to);
+        mAdvancingContent = fullOffscreenAppLeft(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromRightToLeft(int to) {
+        int from = 750;
+
+        mRetreatingSurface = flexOffscreenAppLeft(to);
+        mRetreatingContent = onscreenAppLeft(from);
+        mAdvancingSurface = fullOffscreenAppRight(to);
+        mAdvancingContent = fullOffscreenAppRight(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromRightToCenter(int to) {
+        int from = 750;
+
+        mRetreatingSurface = onscreenAppLeft(to);
+        mRetreatingContent = onscreenAppLeft(from);
+        mAdvancingSurface = fullOffscreenAppRight(to);
+        mAdvancingContent = fullOffscreenAppRight(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private void simulateDragFromRightToRight(int to) {
+        int from = 750;
+
+        mRetreatingSurface = flexOffscreenAppRight(to);
+        mRetreatingContent = fullOffscreenAppRight(from);
+        mAdvancingSurface = onscreenAppLeft(to);
+        mAdvancingContent = onscreenAppLeft(from);
+
+        calculateDimAndParallax(from, to);
+    }
+
+    private Rect flexOffscreenAppLeft(int pos) {
+        return new Rect(pos - (1000 - pos), 0, pos, 1000);
+    }
+
+    private Rect onscreenAppLeft(int pos) {
+        return new Rect(0, 0, pos, 1000);
+    }
+
+    private Rect fullOffscreenAppLeft(int pos) {
+        return new Rect(Math.min(0, pos - 750), 0, pos, 1000);
+    }
+
+    private Rect flexOffscreenAppRight(int pos) {
+        return new Rect(pos, 0, pos * 2, 1000);
+    }
+
+    private Rect onscreenAppRight(int pos) {
+        return new Rect(pos, 0, 1000, 1000);
+    }
+
+    private Rect fullOffscreenAppRight(int pos) {
+        return new Rect(pos, 0, Math.max(pos + 750, 1000), 1000);
+    }
+
+    private void calculateDimAndParallax(int from, int to) {
+        resetParallax();
+        mTopLeftShrink = to < from;
+        mDimmingSide = mFlexSpec.getDimmingSide(to, mockSnapAlgorithm, mIsLeftRightSplit);
+        mDimValue = mFlexSpec.getDimValue(to, mockSnapAlgorithm);
+        mFlexSpec.getParallax(mRetreatingParallax, mAdvancingParallax, to, mockSnapAlgorithm,
+                mIsLeftRightSplit, mDisplayBounds, mRetreatingSurface, mRetreatingContent,
+                mAdvancingSurface, mAdvancingContent, mDimmingSide, mTopLeftShrink);
+    }
+
+    private void resetParallax() {
+        mRetreatingParallax.set(0, 0);
+        mAdvancingParallax.set(0, 0);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt
new file mode 100644
index 0000000..a5f6ced
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2025 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.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReachabilityGestureListenerFactory].
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ReachabilityGestureListenerFactoryTest : ShellTestCase() {
+
+    @Test
+    fun `When invoked a ReachabilityGestureListenerFactory is created`() {
+        runTestScenario { r ->
+            r.invokeCreate()
+
+            r.checkReachabilityGestureListenerCreated()
+        }
+    }
+
+    @Test
+    fun `Right parameters are used for creation`() {
+        runTestScenario { r ->
+            r.invokeCreate()
+
+            r.checkRightParamsAreUsed()
+        }
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) {
+        val robot = ReachabilityGestureListenerFactoryRobotTest()
+        consumer.accept(robot)
+    }
+
+    class ReachabilityGestureListenerFactoryRobotTest {
+
+        companion object {
+            @JvmStatic
+            private val TASK_ID = 1
+
+            @JvmStatic
+            private val TOKEN = mock<WindowContainerToken>()
+        }
+
+        private val transitions: Transitions
+        private val animationHandler: Transitions.TransitionHandler
+        private val factory: ReachabilityGestureListenerFactory
+        private val wctSupplier: WindowContainerTransactionSupplier
+        private val wct: WindowContainerTransaction
+        private lateinit var obtainedResult: Any
+
+        init {
+            transitions = mock<Transitions>()
+            animationHandler = mock<Transitions.TransitionHandler>()
+            wctSupplier = mock<WindowContainerTransactionSupplier>()
+            wct = mock<WindowContainerTransaction>()
+            doReturn(wct).`when`(wctSupplier).get()
+            factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier)
+        }
+
+        fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
+            obtainedResult = factory.createReachabilityGestureListener(taskId, token)
+        }
+
+        fun checkReachabilityGestureListenerCreated(expected: Boolean = true) {
+            assertEquals(expected, obtainedResult is ReachabilityGestureListener)
+        }
+
+        fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
+            with(obtainedResult as ReachabilityGestureListener) {
+                // Click outside the bounds
+                updateActivityBounds(Rect(0, 0, 10, 20))
+                onDoubleTap(motionEventAt(50f, 100f))
+                // WindowContainerTransactionSupplier is invoked to create a
+                // WindowContainerTransaction
+                verify(wctSupplier).get()
+                // Verify the right params are passed to startAppCompatReachability()
+                verify(wct).setReachabilityOffset(
+                    token!!,
+                    taskId,
+                    50,
+                    100
+                )
+                // startTransition() is invoked on Transitions with the right parameters
+                verify(transitions).startTransition(
+                    TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+                    wct,
+                    animationHandler
+                )
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt
new file mode 100644
index 0000000..bc10ea5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2025 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.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
+import com.android.wm.shell.compatui.letterbox.asMode
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReachabilityGestureListener].
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:ReachabilityGestureListenerTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ReachabilityGestureListenerTest : ShellTestCase() {
+
+    @Test
+    fun `Only events outside the bounds are handled`() {
+        runTestScenario { r ->
+            r.updateActivityBounds(Rect(0, 0, 100, 200))
+            r.sendMotionEvent(50, 100)
+
+            r.verifyReachabilityTransitionCreated(expected = false, 50, 100)
+            r.verifyReachabilityTransitionStarted(expected = false)
+            r.verifyEventIsHandled(expected = false)
+
+            r.updateActivityBounds(Rect(0, 0, 10, 50))
+            r.sendMotionEvent(50, 100)
+
+            r.verifyReachabilityTransitionCreated(expected = true, 50, 100)
+            r.verifyReachabilityTransitionStarted(expected = true)
+            r.verifyEventIsHandled(expected = true)
+        }
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) {
+        val robot = ReachabilityGestureListenerRobotTest()
+        consumer.accept(robot)
+    }
+
+    class ReachabilityGestureListenerRobotTest(
+        taskId: Int = TASK_ID,
+        token: WindowContainerToken? = TOKEN
+    ) {
+
+        companion object {
+            @JvmStatic
+            private val TASK_ID = 1
+
+            @JvmStatic
+            private val TOKEN = mock<WindowContainerToken>()
+        }
+
+        private val reachabilityListener: ReachabilityGestureListener
+        private val transitions: Transitions
+        private val animationHandler: Transitions.TransitionHandler
+        private val wctSupplier: WindowContainerTransactionSupplier
+        private val wct: WindowContainerTransaction
+        private var eventHandled = false
+
+        init {
+            transitions = mock<Transitions>()
+            animationHandler = mock<Transitions.TransitionHandler>()
+            wctSupplier = mock<WindowContainerTransactionSupplier>()
+            wct = mock<WindowContainerTransaction>()
+            doReturn(wct).`when`(wctSupplier).get()
+            reachabilityListener =
+                ReachabilityGestureListener(
+                    taskId,
+                    token,
+                    transitions,
+                    animationHandler,
+                    wctSupplier
+                )
+        }
+
+        fun updateActivityBounds(activityBounds: Rect) {
+            reachabilityListener.updateActivityBounds(activityBounds)
+        }
+
+        fun sendMotionEvent(x: Int, y: Int) {
+            eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat()))
+        }
+
+        fun verifyReachabilityTransitionCreated(
+            expected: Boolean,
+            x: Int,
+            y: Int,
+            taskId: Int = TASK_ID,
+            token: WindowContainerToken? = TOKEN
+        ) {
+            verify(wct, expected.asMode()).setReachabilityOffset(
+                token!!,
+                taskId,
+                x,
+                y
+            )
+        }
+
+        fun verifyReachabilityTransitionStarted(expected: Boolean = true) {
+            verify(transitions, expected.asMode()).startTransition(
+                TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+                wct,
+                animationHandler
+            )
+        }
+
+        fun verifyEventIsHandled(expected: Boolean) {
+            assertEquals(expected, eventHandled)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 77cd1e5..e9f92cf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -127,26 +127,6 @@
     }
 
     @Test
-    fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() {
-        val wct = WindowContainerTransaction()
-        val taskId = 1
-        val isLastTask = false
-        whenever(
-                freeformTaskTransitionHandler.startMinimizedModeTransition(
-                    any(),
-                    anyInt(),
-                    anyBoolean(),
-                )
-            )
-            .thenReturn(mock())
-
-        mixedHandler.startMinimizedModeTransition(wct, taskId, isLastTask)
-
-        verify(freeformTaskTransitionHandler)
-            .startMinimizedModeTransition(eq(wct), eq(taskId), eq(isLastTask))
-    }
-
-    @Test
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
     fun startRemoveTransition_callsFreeformTaskTransitionHandler() {
         val wct = WindowContainerTransaction()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 8510441..ed9b97d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -55,6 +55,8 @@
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.spy
 import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
@@ -894,12 +896,12 @@
         val taskId = 1
         val listener = TestListener()
         repo.addActiveTaskListener(listener)
-        repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
+        repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
 
         repo.removeTask(THIRD_DISPLAY, taskId)
 
         assertThat(repo.isActiveTask(taskId)).isFalse()
-        assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
+        assertThat(listener.activeChangesOnThirdDisplay).isEqualTo(2)
     }
 
     @Test
@@ -917,7 +919,7 @@
     fun removeTask_updatesTaskVisibility() {
         repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY)
         val taskId = 1
-        repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
+        repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
 
         repo.removeTask(THIRD_DISPLAY, taskId)
 
@@ -1106,6 +1108,30 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun setTaskInFullImmersiveState_inDesk_savedAsInImmersiveState() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        assertThat(repo.isTaskInFullImmersiveState(6)).isFalse()
+
+        repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+        assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskInFullImmersiveState_inDesk_removedAsInImmersiveState() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+        repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = false)
+
+        assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse()
+    }
+
+    @Test
     fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() {
         repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
 
@@ -1274,14 +1300,146 @@
         assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8))
     }
 
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun setDeskActive() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+
+        repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6)
+
+        assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(6)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun setDeskInactive() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6)
+
+        repo.setDeskInactive(deskId = 6)
+
+        assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun getDeskIdForTask() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+        assertThat(repo.getDeskIdForTask(10)).isEqualTo(6)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_clearsBoundsBeforeMaximize() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        repo.saveBoundsBeforeMaximize(taskId = 10, bounds = Rect(10, 10, 100, 100))
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.removeBoundsBeforeMaximize(taskId = 10)).isNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_clearsBoundsBeforeImmersive() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        repo.saveBoundsBeforeFullImmersive(taskId = 10, bounds = Rect(10, 10, 100, 100))
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.removeBoundsBeforeFullImmersive(taskId = 10)).isNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_removesFromZOrderList() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.getFreeformTasksIdsInDeskInZOrder(deskId = 6)).doesNotContain(10)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_removesFromMinimized() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        repo.minimizeTaskInDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.getMinimizedTaskIdsInDesk(deskId = 6)).doesNotContain(10)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_removesFromImmersive() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_removesFromActiveTasks() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.isActiveTaskInDesk(taskId = 10, deskId = 6)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun removeTaskFromDesk_removesFromVisibleTasks() {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        assertThat(repo.isVisibleTaskInDesk(taskId = 10, deskId = 6)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+    fun removeTaskFromDesk_updatesPersistence() = runTest {
+        repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+        repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+        clearInvocations(persistentRepository)
+
+        repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+        verify(persistentRepository)
+            .addOrUpdateDesktop(
+                userId = eq(DEFAULT_USER_ID),
+                desktopId = eq(6),
+                visibleTasks = any(),
+                minimizedTasks = any(),
+                freeformTasksInZOrder = any(),
+            )
+    }
+
     class TestListener : DesktopRepository.ActiveTasksListener {
         var activeChangesOnDefaultDisplay = 0
         var activeChangesOnSecondaryDisplay = 0
+        var activeChangesOnThirdDisplay = 0
 
         override fun onActiveTasksChanged(displayId: Int) {
             when (displayId) {
                 DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++
                 SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++
+                THIRD_DISPLAY -> activeChangesOnThirdDisplay++
                 else -> fail("Active task listener received unexpected display id: $displayId")
             }
         }
@@ -1290,9 +1448,11 @@
     class TestVisibilityListener : DesktopRepository.VisibleTasksListener {
         var visibleTasksCountOnDefaultDisplay = 0
         var visibleTasksCountOnSecondaryDisplay = 0
+        var visibleTasksCountOnThirdDisplay = 0
 
         var visibleChangesOnDefaultDisplay = 0
         var visibleChangesOnSecondaryDisplay = 0
+        var visibleChangesOnThirdDisplay = 0
 
         override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
             when (displayId) {
@@ -1304,6 +1464,10 @@
                     visibleTasksCountOnSecondaryDisplay = visibleTasksCount
                     visibleChangesOnSecondaryDisplay++
                 }
+                THIRD_DISPLAY -> {
+                    visibleTasksCountOnThirdDisplay = visibleTasksCount
+                    visibleChangesOnThirdDisplay++
+                }
                 else -> fail("Visible task listener received unexpected display id: $displayId")
             }
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index eb4ec11..fcd92ac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -151,8 +151,7 @@
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
-import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.Optional
@@ -234,6 +233,7 @@
     @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
     @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
     @Mock lateinit var recentTasksController: RecentTasksController
+    @Mock lateinit var snapEventHandler: SnapEventHandler
     @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var mockSurface: SurfaceControl
     @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
@@ -246,9 +246,7 @@
     @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
     @Mock private lateinit var mockToast: Toast
     private lateinit var mockitoSession: StaticMockitoSession
-    @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
     @Mock private lateinit var bubbleController: BubbleController
-    @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
     @Mock private lateinit var resources: Resources
     @Mock
     lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener
@@ -287,7 +285,7 @@
     private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
     private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
     private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
-    private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
+    private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 448, 1575, 1611)
     private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
     private val wallpaperToken = MockToken().token()
     private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
@@ -329,8 +327,10 @@
         desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext))
 
         whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
-        whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+        whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() }
         whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() }
+        whenever(exitDesktopTransitionHandler.startTransition(any(), any(), any(), any()))
+            .thenReturn(Binder())
         whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
         whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext)
         whenever(displayController.getDisplay(anyInt())).thenReturn(display)
@@ -380,6 +380,7 @@
         recentsTransitionStateListener = captor.firstValue
 
         controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+        controller.setSnapEventHandler(snapEventHandler)
 
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
@@ -423,7 +424,6 @@
             mockHandler,
             desktopModeEventLogger,
             desktopModeUiEventLogger,
-            desktopTilingDecorViewModel,
             desktopWallpaperActivityTokenProvider,
             Optional.of(bubbleController),
             overviewToDesktopTransitionObserver,
@@ -919,7 +919,10 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    @DisableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+    )
     fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
         taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
         val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
@@ -2041,6 +2044,30 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveToFullscreen_fromDesk_reparentsToTaskDisplayArea() {
+        val task = setUpFreeformTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        wct.assertHop(ReparentPredicate(token = task.token, parentToken = tda.token, toTop = true))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveToFullscreen_fromDesk_deactivatesDesk() {
+        val task = setUpFreeformTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        verify(desksOrganizer).deactivateDesk(wct, deskId = 0)
+    }
+
+    @Test
     fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
         val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2055,6 +2082,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
         whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
         val homeTask = setUpHomeTask()
@@ -2080,6 +2108,60 @@
     }
 
     @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+    )
+    fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity_multiDesksEnabled() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        setUpHomeTask()
+        val task = setUpFreeformTask()
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+        // Removes wallpaper activity when leaving desktop
+        wct.assertReorder(wallpaperToken, toTop = false)
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+    )
+    fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_homeBehindFullscreen_multiDesksEnabled() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        val homeTask = setUpHomeTask()
+        val task = setUpFreeformTask()
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+        // Moves home task behind the fullscreen task
+        val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+        val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true)
+        assertThat(homeReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() {
         whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
@@ -2095,9 +2177,9 @@
         val wct = getLatestExitDesktopWct()
         verify(desktopModeEnterExitTransitionListener)
             .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-        assertThat(wct.hierarchyOps).hasSize(1)
         // Removes wallpaper activity when leaving desktop but doesn't reorder home or the task
-        wct.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+        wct.assertReorder(wallpaperToken, toTop = false)
+        wct.assertWithoutHop(ReorderPredicate(homeTask.token, toTop = null))
     }
 
     @Test
@@ -2115,6 +2197,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
         val homeTask = setUpHomeTask()
         val task = setUpFreeformTask()
@@ -2140,6 +2223,61 @@
     }
 
     @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+    )
+    fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity_multiDesksEnabled() {
+        val homeTask = setUpHomeTask()
+        val task = setUpFreeformTask()
+
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FREEFORM
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        // Removes wallpaper activity when leaving desktop
+        wct.assertReorder(wallpaperToken, toTop = false)
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+    )
+    fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() {
+        val homeTask = setUpHomeTask()
+        val task = setUpFreeformTask()
+
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FREEFORM
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        // Moves home task behind the fullscreen task
+        val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+        val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true)
+        assertThat(homeReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
         val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
@@ -2166,6 +2304,29 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        // Setup task2
+        setUpFreeformTask()
+
+        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
+        assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
+            WINDOWING_MODE_FULLSCREEN
+
+        controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
+        assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        // Does not remove wallpaper activity, as desktop still has a visible desktop task
+        wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
+    }
+
+    @Test
     fun moveToFullscreen_nonExistentTask_doesNothing() {
         controller.moveToFullscreen(999, transitionSource = UNKNOWN)
         verifyExitDesktopWCTNotExecuted()
@@ -2739,7 +2900,10 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    @EnableFlags(
+        FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+    )
     fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() {
         val freeformTask = setUpFreeformTask()
         val pipTask = setUpPipTask(autoEnterEnabled = true)
@@ -2754,10 +2918,34 @@
         val wct = WindowContainerTransaction()
         controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
 
-        // Remove wallpaper operation
-        wct.hierarchyOps.any { hop ->
-            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
-        }
+        // Moves wallpaper activity to back when leaving desktop
+        wct.assertReorder(wallpaperToken, toTop = false)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onDesktopWindowClose_lastWindow_deactivatesDesk() {
+        val task = setUpFreeformTask()
+        val wct = WindowContainerTransaction()
+
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+
+        verify(desksOrganizer).deactivateDesk(wct, deskId = 0)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onDesktopWindowClose_lastWindow_addsPendingDeactivateTransition() {
+        val task = setUpFreeformTask()
+        val wct = WindowContainerTransaction()
+
+        val transition = Binder()
+        val runOnTransitStart =
+            controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        runOnTransitStart(transition)
+
+        verify(desksTransitionsObserver)
+            .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId = 0))
     }
 
     @Test
@@ -2784,6 +2972,48 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onDesktopWindowMinimize_lastWindow_deactivatesDesk() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        whenever(
+                freeformTaskTransitionStarter.startMinimizedModeTransition(
+                    any(),
+                    anyInt(),
+                    anyBoolean(),
+                )
+            )
+            .thenReturn(transition)
+
+        controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+        val captor = argumentCaptor<WindowContainerTransaction>()
+        verify(freeformTaskTransitionStarter)
+            .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true))
+        verify(desksOrganizer).deactivateDesk(captor.firstValue, deskId = 0)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onDesktopWindowMinimize_lastWindow_addsPendingDeactivateTransition() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        whenever(
+                freeformTaskTransitionStarter.startMinimizedModeTransition(
+                    any(),
+                    anyInt(),
+                    anyBoolean(),
+                )
+            )
+            .thenReturn(transition)
+
+        controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+        verify(desksTransitionsObserver)
+            .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0))
+    }
+
+    @Test
     fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
         val task = setUpPipTask(autoEnterEnabled = true)
         val handler = mock(TransitionHandler::class.java)
@@ -2995,6 +3225,24 @@
     }
 
     @Test
+    fun onDesktopWindowMinimize_triesToStopTiling() {
+        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val transition = Binder()
+        whenever(
+                freeformTaskTransitionStarter.startMinimizedModeTransition(
+                    any(),
+                    anyInt(),
+                    anyBoolean(),
+                )
+            )
+            .thenReturn(transition)
+
+        controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+        verify(snapEventHandler).removeTaskIfTiled(eq(DEFAULT_DISPLAY), eq(task.taskId))
+    }
+
+    @Test
     fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
         val homeTask = setUpHomeTask()
         val freeformTask = setUpFreeformTask()
@@ -4035,10 +4283,11 @@
         val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
         assertThat(taskChange.windowingMode)
             .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-        wct.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+        wct.assertReorder(wallpaperToken, toTop = false)
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
         val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
@@ -4062,7 +4311,56 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        // Does not remove wallpaper activity
+        wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        // Moves home task behind the fullscreen task
+        val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+        val fullscreenReorderIndex = wct.indexOfReorder(task2, toTop = true)
+        assertThat(homeReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+        assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+    }
+
+    @Test
+    @EnableFlags(
+        FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+    )
     fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() {
         val freeformTask = setUpFreeformTask()
         val pipTask = setUpPipTask(autoEnterEnabled = true)
@@ -4080,10 +4378,8 @@
         val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()])
         assertThat(taskChange.windowingMode)
             .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-        // Remove wallpaper operation
-        wct.hierarchyOps.any { hop ->
-            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
-        }
+        // Moves wallpaper activity to back when leaving desktop
+        wct.assertReorder(wallpaperToken, toTop = false)
     }
 
     @Test
@@ -4483,7 +4779,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
         val rectAfterEnd = Rect(100, 50, 500, 1150)
         verify(transitions)
@@ -4521,7 +4816,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
 
         verify(transitions)
@@ -4561,7 +4855,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
 
         verify(transitions)
@@ -4602,7 +4895,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
 
         // Assert the task exits desktop mode
@@ -4640,7 +4932,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
 
         // Assert bounds set to stable bounds
@@ -4696,7 +4987,6 @@
             validDragArea = Rect(0, 50, 2000, 2000),
             dragStartBounds = Rect(),
             motionEvent,
-            desktopWindowDecoration,
         )
 
         // Assert that task is NOT updated via WCT
@@ -5116,7 +5406,6 @@
             SnapPosition.LEFT,
             ResizeTrigger.SNAP_LEFT_MENU,
             InputMethod.TOUCH,
-            desktopWindowDecoration,
         )
         // Assert bounds set to stable bounds
         val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
@@ -5162,7 +5451,6 @@
             SnapPosition.LEFT,
             ResizeTrigger.SNAP_LEFT_MENU,
             InputMethod.TOUCH,
-            desktopWindowDecoration,
         )
         // Assert that task is NOT updated via WCT
         verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -5206,7 +5494,6 @@
             currentDragBounds,
             preDragBounds,
             motionEvent,
-            desktopWindowDecoration,
         )
         val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
         assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
@@ -5236,7 +5523,6 @@
             currentDragBounds,
             preDragBounds,
             motionEvent,
-            desktopWindowDecoration,
         )
         verify(mReturnToDragStartAnimator)
             .start(
@@ -5261,7 +5547,6 @@
             SnapPosition.LEFT,
             ResizeTrigger.SNAP_LEFT_MENU,
             InputMethod.MOUSE,
-            desktopWindowDecoration,
         )
 
         // Assert that task is NOT updated via WCT
@@ -5288,7 +5573,6 @@
             SnapPosition.LEFT,
             ResizeTrigger.SNAP_LEFT_MENU,
             InputMethod.MOUSE,
-            desktopWindowDecoration,
         )
 
         // Assert bounds set to half of the stable bounds
@@ -6341,15 +6625,46 @@
     assertThat(hierarchyOps.none(predicate)).isTrue()
 }
 
+private fun WindowContainerTransaction.indexOfReorder(
+    task: RunningTaskInfo,
+    toTop: Boolean? = null,
+): Int {
+    val hop = hierarchyOps.singleOrNull(ReorderPredicate(task.token, toTop)) ?: return -1
+    return hierarchyOps.indexOf(hop)
+}
+
+private class ReorderPredicate(val token: WindowContainerToken, val toTop: Boolean? = null) :
+    ((WindowContainerTransaction.HierarchyOp) -> Boolean) {
+    override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean =
+        hop.type == HIERARCHY_OP_TYPE_REORDER &&
+            (toTop == null || hop.toTop == toTop) &&
+            hop.container == token.asBinder()
+}
+
+private class ReparentPredicate(
+    val token: WindowContainerToken,
+    val parentToken: WindowContainerToken,
+    val toTop: Boolean? = null,
+) : ((WindowContainerTransaction.HierarchyOp) -> Boolean) {
+    override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean =
+        hop.isReparent &&
+            (toTop == null || hop.toTop == toTop) &&
+            hop.container == token.asBinder() &&
+            hop.newParent == parentToken.asBinder()
+}
+
 private fun WindowContainerTransaction.assertReorder(
     task: RunningTaskInfo,
     toTop: Boolean? = null,
 ) {
-    assertHop { hop ->
-        hop.type == HIERARCHY_OP_TYPE_REORDER &&
-            (toTop == null || hop.toTop == toTop) &&
-            hop.container == task.token.asBinder()
-    }
+    assertReorder(task.token, toTop)
+}
+
+private fun WindowContainerTransaction.assertReorder(
+    token: WindowContainerToken,
+    toTop: Boolean? = null,
+) {
+    assertHop(ReorderPredicate(token, toTop))
 }
 
 private fun WindowContainerTransaction.assertReorderAt(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt
new file mode 100644
index 0000000..aa4e9aa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2025 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.desktopmode.desktopwallpaperactivity
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display.DEFAULT_DISPLAY
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test class for [DesktopWallpaperActivityTokenProvider]
+ *
+ * Usage: atest WMShellUnitTests:DesktopWallpaperActivityTokenProviderTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopWallpaperActivityTokenProviderTest : ShellTestCase() {
+
+    private lateinit var provider: DesktopWallpaperActivityTokenProvider
+    private val DEFAULT_DISPLAY = 0
+    private val SECONDARY_DISPLAY = 1
+
+    @Before
+    fun setUp() {
+        provider = DesktopWallpaperActivityTokenProvider()
+    }
+
+    @Test
+    fun setToken_setsTokenForDisplay() {
+        val token = MockToken().token()
+
+        provider.setToken(token, DEFAULT_DISPLAY)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token)
+    }
+
+    @Test
+    fun setToken_overwritesExistingTokenForDisplay() {
+        val token1 = MockToken().token()
+        val token2 = MockToken().token()
+
+        provider.setToken(token1, DEFAULT_DISPLAY)
+        provider.setToken(token2, DEFAULT_DISPLAY)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token2)
+    }
+
+    @Test
+    fun getToken_returnsNullForNonExistentDisplay() {
+        assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull()
+    }
+
+    @Test
+    fun removeToken_removesTokenForDisplay() {
+        val token = MockToken().token()
+
+        provider.setToken(token, DEFAULT_DISPLAY)
+        provider.removeToken(DEFAULT_DISPLAY)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+    }
+
+    @Test
+    fun removeToken_withToken_removesTokenForDisplay() {
+        val token = MockToken().token()
+
+        provider.setToken(token, DEFAULT_DISPLAY)
+        provider.removeToken(token)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+    }
+
+    @Test
+    fun removeToken_doesNothingForNonExistentDisplay() {
+        provider.removeToken(SECONDARY_DISPLAY)
+
+        assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull()
+    }
+
+    @Test
+    fun removeToken_withNonExistentToken_doesNothing() {
+        val token1 = MockToken().token()
+        val token2 = MockToken().token()
+
+        provider.setToken(token1, DEFAULT_DISPLAY)
+        provider.removeToken(token2)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1)
+    }
+
+    @Test
+    fun multipleDisplays_tokensAreIndependent() {
+        val token1 = MockToken().token()
+        val token2 = MockToken().token()
+
+        provider.setToken(token1, DEFAULT_DISPLAY)
+        provider.setToken(token2, SECONDARY_DISPLAY)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1)
+        assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2)
+
+        provider.removeToken(DEFAULT_DISPLAY)
+
+        assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+        assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
index 9f09e3f..4dcf669 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
@@ -20,6 +20,7 @@
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
@@ -177,4 +178,70 @@
             assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId)
             assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId)
         }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onTransitionReady_deactivateDesk_updatesRepository() {
+        val transition = Binder()
+        val deskChange = Change(mock(), mock())
+        whenever(mockDesksOrganizer.isDeskChange(deskChange, deskId = 5)).thenReturn(true)
+        val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+        repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+        repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+
+        observer.addPendingTransition(deactivateTransition)
+        observer.onTransitionReady(
+            transition = transition,
+            info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { addChange(deskChange) },
+        )
+
+        assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onTransitionReady_deactivateDeskWithExitingTask_updatesRepository() {
+        val transition = Binder()
+        val exitingTask = createFreeformTask(DEFAULT_DISPLAY)
+        val exitingTaskChange = Change(mock(), mock()).apply { taskInfo = exitingTask }
+        whenever(mockDesksOrganizer.getDeskAtEnd(exitingTaskChange)).thenReturn(null)
+        val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+        repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+        repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+        repository.addTaskToDesk(
+            displayId = DEFAULT_DISPLAY,
+            deskId = 5,
+            taskId = exitingTask.taskId,
+            isVisible = true,
+        )
+        assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isTrue()
+
+        observer.addPendingTransition(deactivateTransition)
+        observer.onTransitionReady(
+            transition = transition,
+            info =
+                TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply {
+                    addChange(exitingTaskChange)
+                },
+        )
+
+        assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun onTransitionReady_deactivateDeskWithoutVisibleChange_updatesRepository() {
+        val transition = Binder()
+        val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+        repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+        repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+
+        observer.addPendingTransition(deactivateTransition)
+        observer.onTransitionReady(
+            transition = transition,
+            info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0),
+        )
+
+        assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index 4d4b153..8b10ca1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -23,11 +23,13 @@
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
 import android.window.WindowContainerTransaction.HierarchyOp
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.google.common.truth.Truth.assertThat
@@ -104,54 +106,45 @@
 
     @Test
     fun testOnTaskVanished_removesRoot() {
-        val callback = FakeOnCreateCallback()
-        organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        val desk = createDesk()
 
-        organizer.onTaskVanished(freeformRoot)
+        organizer.onTaskVanished(desk.taskInfo)
 
-        assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse()
+        assertThat(organizer.roots.contains(desk.deskId)).isFalse()
     }
 
     @Test
     fun testDesktopWindowAppearsInDesk() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-        val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+        val desk = createDesk()
+        val child = createFreeformTask().apply { parentTaskId = desk.deskId }
 
         organizer.onTaskAppeared(child, SurfaceControl())
 
-        assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId)
+        assertThat(desk.children).contains(child.taskId)
     }
 
     @Test
     fun testDesktopWindowDisappearsFromDesk() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-        val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+        val desk = createDesk()
+        val child = createFreeformTask().apply { parentTaskId = desk.deskId }
 
         organizer.onTaskAppeared(child, SurfaceControl())
         organizer.onTaskVanished(child)
 
-        assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId)
+        assertThat(desk.children).doesNotContain(child.taskId)
     }
 
     @Test
     fun testRemoveDesk() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        val desk = createDesk()
 
         val wct = WindowContainerTransaction()
-        organizer.removeDesk(wct, freeformRoot.taskId)
+        organizer.removeDesk(wct, desk.deskId)
 
         assertThat(
                 wct.hierarchyOps.any { hop ->
                     hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
-                        hop.container == freeformRoot.token.asBinder()
+                        hop.container == desk.taskInfo.token.asBinder()
                 }
             )
             .isTrue()
@@ -167,25 +160,23 @@
 
     @Test
     fun testActivateDesk() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        val desk = createDesk()
 
         val wct = WindowContainerTransaction()
-        organizer.activateDesk(wct, freeformRoot.taskId)
+        organizer.activateDesk(wct, desk.deskId)
 
         assertThat(
                 wct.hierarchyOps.any { hop ->
                     hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER &&
                         hop.toTop &&
-                        hop.container == freeformRoot.token.asBinder()
+                        hop.container == desk.taskInfo.token.asBinder()
                 }
             )
             .isTrue()
         assertThat(
                 wct.hierarchyOps.any { hop ->
                     hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
-                        hop.container == freeformRoot.token.asBinder()
+                        hop.container == desk.taskInfo.token.asBinder()
                 }
             )
             .isTrue()
@@ -201,20 +192,18 @@
 
     @Test
     fun testMoveTaskToDesk() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        val desk = createDesk()
 
         val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
         val wct = WindowContainerTransaction()
-        organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask)
+        organizer.moveTaskToDesk(wct, desk.deskId, desktopTask)
 
         assertThat(
                 wct.hierarchyOps.any { hop ->
                     hop.isReparent &&
                         hop.toTop &&
                         hop.container == desktopTask.token.asBinder() &&
-                        hop.newParent == freeformRoot.token.asBinder()
+                        hop.newParent == desk.taskInfo.token.asBinder()
                 }
             )
             .isTrue()
@@ -240,17 +229,15 @@
 
     @Test
     fun testGetDeskAtEnd() {
-        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
-        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
-        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        val desk = createDesk()
 
-        val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+        val task = createFreeformTask().apply { parentTaskId = desk.deskId }
         val endDesk =
             organizer.getDeskAtEnd(
                 TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
             )
 
-        assertThat(endDesk).isEqualTo(freeformRoot.taskId)
+        assertThat(endDesk).isEqualTo(desk.deskId)
     }
 
     @Test
@@ -273,6 +260,47 @@
         assertThat(isActive).isTrue()
     }
 
+    @Test
+    fun deactivateDesk_clearsLaunchRoot() {
+        val wct = WindowContainerTransaction()
+        val desk = createDesk()
+        organizer.activateDesk(wct, desk.deskId)
+
+        organizer.deactivateDesk(wct, desk.deskId)
+
+        assertThat(
+                wct.hierarchyOps.any { hop ->
+                    hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
+                        hop.container == desk.taskInfo.token.asBinder() &&
+                        hop.windowingModes == null &&
+                        hop.activityTypes == null
+                }
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun isDeskChange() {
+        val desk = createDesk()
+
+        assertThat(
+                organizer.isDeskChange(
+                    TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply {
+                        taskInfo = desk.taskInfo
+                    },
+                    desk.deskId,
+                )
+            )
+            .isTrue()
+    }
+
+    private fun createDesk(): DeskRoot {
+        organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+        val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+        organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+        return organizer.roots[freeformRoot.taskId]
+    }
+
     private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
         var deskId: Int? = null
         val created: Boolean
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index 8e0381e..0c19529 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.when;
@@ -26,6 +27,7 @@
 import static org.mockito.kotlin.VerificationKt.times;
 import static org.mockito.kotlin.VerificationKt.verify;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Matrix;
@@ -44,15 +46,19 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Unit test against {@link PipScheduler}
  */
@@ -77,6 +83,8 @@
     @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
     @Mock private SurfaceControl.Transaction mMockTransaction;
     @Mock private PipAlphaAnimator mMockAlphaAnimator;
+    @Mock private SplitScreenController mMockSplitScreenController;
+
     @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
     @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
 
@@ -93,7 +101,8 @@
                 .thenReturn(mMockTransaction);
 
         mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
-                mMockPipTransitionState, mMockPipDesktopState);
+                mMockPipTransitionState, Optional.of(mMockSplitScreenController),
+                mMockPipDesktopState);
         mPipScheduler.setPipTransitionController(mMockPipTransitionController);
         mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
         mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) ->
@@ -119,12 +128,18 @@
         assertNotNull(mRunnableArgumentCaptor.getValue());
         mRunnableArgumentCaptor.getValue().run();
 
-        verify(mMockPipTransitionController, never()).startExpandTransition(any());
+        verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean());
     }
 
     @Test
-    public void scheduleExitPipViaExpand_exitTransitionCalled() {
+    public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() {
         setMockPipTaskToken();
+        ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+        when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+        // Make sure task with the id = 1 isn't in split-screen.
+        when(mMockSplitScreenController.isTaskInSplitScreen(
+                ArgumentMatchers.eq(1))).thenReturn(false);
 
         mPipScheduler.scheduleExitPipViaExpand();
 
@@ -132,7 +147,29 @@
         assertNotNull(mRunnableArgumentCaptor.getValue());
         mRunnableArgumentCaptor.getValue().run();
 
-        verify(mMockPipTransitionController, times(1)).startExpandTransition(any());
+        verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
+    }
+
+    @Test
+    public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() {
+        setMockPipTaskToken();
+        ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+        when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+        // Make sure task with the id = 1 is in split-screen.
+        when(mMockSplitScreenController.isTaskInSplitScreen(
+                ArgumentMatchers.eq(1))).thenReturn(true);
+
+        mPipScheduler.scheduleExitPipViaExpand();
+
+        verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture());
+        assertNotNull(mRunnableArgumentCaptor.getValue());
+        mRunnableArgumentCaptor.getValue().run();
+
+        // We need to both prepare the split screen with the last parent and start expanding.
+        verify(mMockSplitScreenController,
+                times(1)).prepareEnterSplitScreen(any(), any(), anyInt());
+        verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
     }
 
     @Test
@@ -259,4 +296,10 @@
     private void setMockPipTaskToken() {
         when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken);
     }
+
+    private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) {
+        final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+        taskInfo.lastParentTaskIdBeforePip = lastParentId;
+        return taskInfo;
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java
new file mode 100644
index 0000000..2e389b7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2017 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.pip2.phone;
+
+import static android.view.MotionEvent.ACTION_BUTTON_PRESS;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.SystemClock;
+import android.testing.AndroidTestingRunner;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class PipTouchStateTest extends ShellTestCase {
+
+    private PipTouchState mTouchState;
+    private CountDownLatch mDoubleTapCallbackTriggeredLatch;
+    private CountDownLatch mHoverExitCallbackTriggeredLatch;
+    private TestShellExecutor mMainExecutor;
+
+    @Before
+    public void setUp() throws Exception {
+        mMainExecutor = new TestShellExecutor();
+        mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
+        mHoverExitCallbackTriggeredLatch = new CountDownLatch(1);
+        mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
+                mDoubleTapCallbackTriggeredLatch::countDown,
+                mHoverExitCallbackTriggeredLatch::countDown,
+                mMainExecutor);
+        assertFalse(mTouchState.isDoubleTap());
+        assertFalse(mTouchState.isWaitingForDoubleTap());
+    }
+
+    @Test
+    public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() {
+        final long currentTime = SystemClock.uptimeMillis();
+
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0));
+        assertFalse(mTouchState.isDoubleTap());
+        assertFalse(mTouchState.isWaitingForDoubleTap());
+        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+    }
+
+    @Test
+    public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception {
+        final long currentTime = SystemClock.uptimeMillis();
+
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+        assertFalse(mTouchState.isDoubleTap());
+        assertTrue(mTouchState.isWaitingForDoubleTap());
+
+        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
+        mTouchState.scheduleDoubleTapTimeoutCallback();
+
+        mMainExecutor.flushAll();
+        assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
+    }
+
+    @Test
+    public void testDoubleTapDrag_doubleTapCanceled() {
+        final long currentTime = SystemClock.uptimeMillis();
+
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500));
+        assertTrue(mTouchState.isDragging());
+        assertFalse(mTouchState.isDoubleTap());
+        assertFalse(mTouchState.isWaitingForDoubleTap());
+        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+    }
+
+    @Test
+    public void testDoubleTap_doubleTapRegistered() {
+        final long currentTime = SystemClock.uptimeMillis();
+
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN,
+                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0));
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+        assertTrue(mTouchState.isDoubleTap());
+        assertFalse(mTouchState.isWaitingForDoubleTap());
+        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+    }
+
+    @Test
+    public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception {
+        mTouchState.scheduleHoverExitTimeoutCallback();
+        mMainExecutor.flushAll();
+        assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0);
+    }
+
+    @Test
+    public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception {
+        mTouchState.scheduleHoverExitTimeoutCallback();
+        assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+    }
+
+    @Test
+    public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception {
+        mTouchState.scheduleHoverExitTimeoutCallback();
+        mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(),
+                0, 0));
+        mMainExecutor.flushAll();
+        assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+    }
+
+    private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) {
+        return MotionEvent.obtain(0, eventTime, action, x, y, 0);
+    }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
new file mode 100644
index 0000000..2a22842
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2025 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.pip2.phone.transition;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.kotlin.VerificationKt.times;
+import static org.mockito.kotlin.VerificationKt.verify;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.TransitionInfoBuilder;
+import com.android.wm.shell.util.StubTransaction;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Unit test against {@link PipExpandHandler}
+ */
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class PipExpandHandlerTest {
+    @Mock private Context mMockContext;
+    @Mock private PipBoundsState mMockPipBoundsState;
+    @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
+    @Mock private PipTransitionState mMockPipTransitionState;
+    @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
+    @Mock private SplitScreenController mMockSplitScreenController;
+
+    @Mock private IBinder mMockTransitionToken;
+    @Mock private TransitionRequestInfo mMockRequestInfo;
+    @Mock private StubTransaction mStartT;
+    @Mock private StubTransaction mFinishT;
+    @Mock private SurfaceControl mPipLeash;
+
+    @Mock private PipExpandAnimator mMockPipExpandAnimator;
+
+    @Surface.Rotation
+    private static final int DISPLAY_ROTATION = Surface.ROTATION_0;
+
+    private static final float SNAP_FRACTION = 1.5f;
+    private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100);
+    private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000);
+    private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000);
+
+    private PipExpandHandler mPipExpandHandler;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS);
+        when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION);
+        when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION);
+
+        mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState,
+                mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState,
+                Optional.of(mMockSplitScreenController));
+        mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction,
+                finishTransaction, baseBounds, startBounds, endBounds,
+                sourceRectHint, rotation) -> mMockPipExpandAnimator);
+    }
+
+    @Test
+    public void handleRequest_returnNull() {
+        // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be
+        // returning any non-null WCT
+        WindowContainerTransaction wct = mPipExpandHandler.handleRequest(
+                mMockTransitionToken, mMockRequestInfo);
+        assertNull(wct);
+    }
+
+    @Test
+    public void startAnimation_transitExit_startExpandAnimator() {
+        final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo(
+                1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build());
+
+        final TransitionInfo info = getExpandFromPipTransitionInfo(
+                TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */);
+        final WindowContainerToken pipToken = pipTaskInfo.getToken();
+        when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+        mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+                (wct) -> {});
+
+        verify(mMockPipExpandAnimator, times(1)).start();
+        verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+    }
+
+    @Test
+    public void startAnimation_transitExitToSplit_startExpandAnimator() {
+        // The task info of the task that was pinned while we were in PiP.
+        final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN,
+                new PictureInPictureParams.Builder().build()).getToken();
+        when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+        // Change representing the ActivityRecord we are animating in the multi-activity PiP case;
+        // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token.
+        final TransitionInfo info = getExpandFromPipTransitionInfo(
+                TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */);
+
+        mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+                (wct) -> {});
+
+        verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT));
+        verify(mMockPipExpandAnimator, times(1)).start();
+        verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+    }
+
+    private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type,
+            @Nullable ActivityManager.RunningTaskInfo pipTaskInfo,
+            @Nullable WindowContainerToken lastParent, boolean toSplit) {
+        final TransitionInfo info = new TransitionInfoBuilder(type)
+                .addChange(TRANSIT_CHANGE, pipTaskInfo).build();
+        final TransitionInfo.Change pipChange = info.getChanges().getFirst();
+        pipChange.setRotation(DISPLAY_ROTATION,
+                WindowConfiguration.ROTATION_UNDEFINED);
+        pipChange.setStartAbsBounds(PIP_BOUNDS);
+        pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS);
+        pipChange.setLeash(mPipLeash);
+        pipChange.setLastParent(lastParent);
+        return info;
+    }
+
+    private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId,
+            int windowingMode, PictureInPictureParams params) {
+        ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+        taskInfo.taskId = taskId;
+        taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+        taskInfo.token = mock(WindowContainerToken.class);
+        taskInfo.baseIntent = mock(Intent.class);
+        taskInfo.pictureInPictureParams = params;
+        return taskInfo;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 5028479..a546b3e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -107,6 +107,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class RecentTasksControllerTest extends ShellTestCase {
+    private static final String SYSTEM_UI_PACKAGE_NAME = "com.android.systemui";
 
     @Mock
     private Context mContext;
@@ -582,6 +583,19 @@
     @Test
     @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
             Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+    public void onTaskAdded_orDesktopWallpaperActivity_doesNotTriggerOnRunningTaskAppeared()
+            throws Exception {
+        RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+
+        mRecentTasksControllerReal.onTaskAdded(taskInfo);
+
+        verify(mRecentTasksListener, never()).onRunningTaskAppeared(any());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
     public void taskWindowingModeChanged_desktopRunningAppsEnabled_triggersOnRunningTaskChanged()
             throws Exception {
         mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
@@ -593,6 +607,19 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+    public void taskInfoChanged_forDesktopWallpaperActivity_doesNotTriggerOnRunningTaskChanged()
+            throws Exception {
+        RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+
+        mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo);
+
+        verify(mRecentTasksListener, never()).onRunningTaskChanged(any());
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS)
     public void
@@ -619,6 +646,20 @@
         verify(mRecentTasksListener).onRunningTaskVanished(taskInfo);
     }
 
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+    public void onTaskRemoved_forDesktopWallpaperActivity_doesNotTriggerOnRunningTaskVanished()
+            throws Exception {
+        RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+
+        mRecentTasksControllerReal.onTaskRemoved(taskInfo);
+
+        verify(mRecentTasksListener, never()).onRunningTaskVanished(any());
+    }
+
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS)
@@ -659,6 +700,18 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    public void onDesktopWallpaperActivityMovedToFront_doesNotTriggerOnTaskMovedToFront()
+            throws Exception {
+        RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+
+        mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
+
+        verify(mRecentTasksListener, never()).onTaskMovedToFront(any());
+    }
+
+    @Test
     public void getNullSplitBoundsNonSplitTask() {
         SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3);
         assertNull("splitBounds should be null for non-split task", sb);
@@ -829,16 +882,25 @@
      * Helper to create a running task with a given task id.
      */
     private RunningTaskInfo makeRunningTaskInfo(int taskId) {
+        return makeRunningTaskInfo(taskId, new ComponentName("com." + taskId, "Activity" + taskId));
+    }
+
+    private RunningTaskInfo makeRunningTaskInfo(int taskId, ComponentName intentComponent) {
         RunningTaskInfo info = new RunningTaskInfo();
         info.taskId = taskId;
         info.realActivity = new ComponentName("testPackage", "testClass");
         Intent intent = new Intent();
-        intent.setComponent(new ComponentName("com." + taskId, "Activity" + taskId));
+        intent.setComponent(intentComponent);
         info.baseIntent = intent;
         info.lastNonFullscreenBounds = new Rect();
         return info;
     }
 
+    private RunningTaskInfo makeDesktopWallpaperActivityTaskInfo(int taskId) {
+        return makeRunningTaskInfo(taskId, new ComponentName(SYSTEM_UI_PACKAGE_NAME,
+                DesktopWallpaperActivity.class.getName()));
+    }
+
     /**
      * Helper to set the raw task list on the controller.
      */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
index efb91c5..180a691 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
@@ -16,23 +16,33 @@
 
 package com.android.wm.shell.shared.bubbles
 
+import android.content.Context
 import android.graphics.Rect
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFails
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.test.assertFails
 
 /** Unit tests for [DropTargetManager]. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DropTargetManagerTest {
 
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
+    private val context = getApplicationContext<Context>()
     private lateinit var dropTargetManager: DropTargetManager
     private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener
-    private val dropTarget = Rect(0, 0, 0, 0)
+    private lateinit var container: FrameLayout
 
     // create 3 drop zones that are horizontally next to each other
     // -------------------------------------------------
@@ -43,15 +53,20 @@
     // |               |               |               |
     // -------------------------------------------------
     private val bubbleLeftDragZone =
-        DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget)
+        DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = Rect(0, 0, 50, 200))
     private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100))
     private val bubbleRightDragZone =
-        DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget)
+        DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150))
+
+    private val dropTargetView: View
+        get() = container.getChildAt(0)
 
     @Before
     fun setUp() {
+        container = FrameLayout(context)
         dragZoneChangedListener = FakeDragZoneChangedListener()
-        dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener)
+        dropTargetManager =
+            DropTargetManager(context, container, isLayoutRtl = false, dragZoneChangedListener)
     }
 
     @Test
@@ -79,17 +94,21 @@
             DraggedObject.Bubble(BubbleBarLocation.LEFT),
             listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
         )
-        dropTargetManager.onDragUpdated(
-            bubbleRightDragZone.bounds.centerX(),
-            bubbleRightDragZone.bounds.centerY()
-        )
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleRightDragZone.bounds.centerX(),
+                bubbleRightDragZone.bounds.centerY()
+            )
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
         assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
 
-        dropTargetManager.onDragUpdated(
-            dismissDragZone.bounds.centerX(),
-            dismissDragZone.bounds.centerY()
-        )
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                dismissDragZone.bounds.centerX(),
+                dismissDragZone.bounds.centerY()
+            )
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
         assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
     }
@@ -100,10 +119,12 @@
             DraggedObject.Bubble(BubbleBarLocation.LEFT),
             listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
         )
-        dropTargetManager.onDragUpdated(
-            bubbleLeftDragZone.bounds.centerX(),
-            bubbleLeftDragZone.bounds.centerY()
-        )
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleLeftDragZone.bounds.centerX(),
+                bubbleLeftDragZone.bounds.centerY()
+            )
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isNull()
         assertThat(dragZoneChangedListener.toDragZone).isNull()
     }
@@ -118,7 +139,9 @@
         val pointY = 200
         assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse()
         assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse()
-        dropTargetManager.onDragUpdated(pointX, pointY)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(pointX, pointY)
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isNull()
         assertThat(dragZoneChangedListener.toDragZone).isNull()
     }
@@ -135,27 +158,30 @@
 
         // drag to a point that is within both the bubble right zone and split zone
         val (pointX, pointY) =
-            Pair(
-                bubbleRightDragZone.bounds.centerX(),
-                bubbleRightDragZone.bounds.centerY()
-            )
+            Pair(bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY())
         assertThat(splitDragZone.contains(pointX, pointY)).isTrue()
-        dropTargetManager.onDragUpdated(pointX, pointY)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(pointX, pointY)
+        }
         // verify we dragged to the bubble right zone because that has higher priority than split
         assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
         assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
 
-        dropTargetManager.onDragUpdated(
-            bubbleRightDragZone.bounds.centerX(),
-            150 // below the bubble and dismiss drag zones but within split
-        )
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleRightDragZone.bounds.centerX(),
+                150 // below the bubble and dismiss drag zones but within split
+            )
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
         assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone)
 
         val (dismissPointX, dismissPointY) =
             Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY())
         assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue()
-        dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
+        }
         assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone)
         assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
     }
@@ -166,7 +192,9 @@
             DraggedObject.Bubble(BubbleBarLocation.LEFT),
             listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
         )
-        dropTargetManager.onDragEnded()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragEnded()
+        }
         dropTargetManager.onDragUpdated(
             bubbleRightDragZone.bounds.centerX(),
             bubbleRightDragZone.bounds.centerY()
@@ -175,6 +203,129 @@
         assertThat(dragZoneChangedListener.toDragZone).isNull()
     }
 
+    @Test
+    fun onDragStarted_dropTargetAddedToContainer() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone)
+        )
+        assertThat(container.childCount).isEqualTo(1)
+        assertThat(dropTargetView.alpha).isEqualTo(0)
+    }
+
+    @Test
+    fun onDragEnded_dropTargetRemovedFromContainer() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone)
+        )
+        assertThat(container.childCount).isEqualTo(1)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragEnded()
+            animatorTestRule.advanceTimeBy(250)
+        }
+        assertThat(container.childCount).isEqualTo(0)
+    }
+
+    @Test
+    fun startNewDrag_beforeDropTargetRemoved() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(bubbleLeftDragZone, bubbleRightDragZone)
+        )
+        assertThat(container.childCount).isEqualTo(1)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragEnded()
+            // advance the timer by 100ms so the animation doesn't complete
+            animatorTestRule.advanceTimeBy(100)
+        }
+        assertThat(container.childCount).isEqualTo(1)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragStarted(
+                DraggedObject.Bubble(BubbleBarLocation.LEFT),
+                listOf(bubbleLeftDragZone, bubbleRightDragZone)
+            )
+        }
+        assertThat(container.childCount).isEqualTo(1)
+    }
+
+    @Test
+    fun updateDragZone_withDropTarget_dropTargetUpdated() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone)
+        )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleRightDragZone.bounds.centerX(),
+                bubbleRightDragZone.bounds.centerY()
+            )
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        assertThat(dropTargetView.alpha).isEqualTo(1)
+        verifyDropTargetPosition(bubbleRightDragZone.dropTarget)
+    }
+
+    @Test
+    fun updateDragZone_withoutDropTarget_dropTargetHidden() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone)
+        )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                dismissDragZone.bounds.centerX(),
+                dismissDragZone.bounds.centerY()
+            )
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        assertThat(dropTargetView.alpha).isEqualTo(0)
+    }
+
+    @Test
+    fun updateDragZone_betweenZonesWithDropTarget_dropTargetUpdated() {
+        dropTargetManager.onDragStarted(
+            DraggedObject.Bubble(BubbleBarLocation.LEFT),
+            listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone)
+        )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleRightDragZone.bounds.centerX(),
+                bubbleRightDragZone.bounds.centerY()
+            )
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        assertThat(dropTargetView.alpha).isEqualTo(1)
+        verifyDropTargetPosition(bubbleRightDragZone.dropTarget)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            dropTargetManager.onDragUpdated(
+                bubbleLeftDragZone.bounds.centerX(),
+                bubbleLeftDragZone.bounds.centerY()
+            )
+            animatorTestRule.advanceTimeBy(250)
+        }
+
+        assertThat(dropTargetView.alpha).isEqualTo(1)
+        verifyDropTargetPosition(bubbleLeftDragZone.dropTarget)
+    }
+
+    private fun verifyDropTargetPosition(rect: Rect) {
+        assertThat(dropTargetView.scaleX).isEqualTo(rect.width())
+        assertThat(dropTargetView.scaleY).isEqualTo(rect.height())
+        assertThat(dropTargetView.translationX).isEqualTo(rect.exactCenterX())
+        assertThat(dropTargetView.translationY).isEqualTo(rect.exactCenterY())
+    }
+
     private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {
         var initialDragZone: DragZone? = null
         var fromDragZone: DragZone? = null
@@ -183,6 +334,7 @@
         override fun onInitialDragZoneSet(dragZone: DragZone) {
             initialDragZone = dragZone
         }
+
         override fun onDragZoneChanged(from: DragZone, to: DragZone) {
             fromDragZone = from
             toDragZone = to
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 741a0fd..4082ffd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -22,8 +22,6 @@
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.Presubmit
 import android.platform.test.flag.junit.SetFlagsRule
-import android.provider.Settings
-import android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES
 import android.window.DesktopModeFlags
 import androidx.test.filters.SmallTest
 import com.android.internal.R
@@ -63,14 +61,12 @@
         doReturn(context.contentResolver).whenever(mockContext).contentResolver
         resetDesktopModeFlagsCache()
         resetEnforceDeviceRestriction()
-        resetFlagOverride()
     }
 
     @After
     fun tearDown() {
         resetDesktopModeFlagsCache()
         resetEnforceDeviceRestriction()
-        resetFlagOverride()
     }
 
     @DisableFlags(
@@ -246,18 +242,11 @@
         cachedToggleOverride.set(null, null)
     }
 
-    private fun resetFlagOverride() {
-        Settings.Global.putString(
-            context.contentResolver,
-            DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null
-        )
-    }
-
     private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) {
-        Settings.Global.putInt(
-            context.contentResolver,
-            DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting
-        )
+        val cachedToggleOverride =
+            DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride")
+        cachedToggleOverride.isAccessible = true
+        cachedToggleOverride.set(null, override)
     }
 
     private fun setDeviceEligibleForDesktopMode(eligible: Boolean) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index da41a23..d8d45c0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -484,7 +484,6 @@
             eq(SnapPosition.LEFT),
             eq(ResizeTrigger.SNAP_LEFT_MENU),
             eq(InputMethod.UNKNOWN_INPUT_METHOD),
-            eq(decor)
         )
     }
 
@@ -520,7 +519,6 @@
             eq(SnapPosition.LEFT),
             eq(ResizeTrigger.SNAP_LEFT_MENU),
             eq(InputMethod.UNKNOWN_INPUT_METHOD),
-            eq(decor),
         )
     }
 
@@ -542,7 +540,6 @@
                 eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
-                eq(decor),
             )
     }
 
@@ -562,7 +559,6 @@
             eq(SnapPosition.RIGHT),
             eq(ResizeTrigger.SNAP_RIGHT_MENU),
             eq(InputMethod.UNKNOWN_INPUT_METHOD),
-            eq(decor),
         )
     }
 
@@ -598,7 +594,6 @@
             eq(SnapPosition.RIGHT),
             eq(ResizeTrigger.SNAP_RIGHT_MENU),
             eq(InputMethod.UNKNOWN_INPUT_METHOD),
-            eq(decor),
         )
     }
 
@@ -620,7 +615,6 @@
                 eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
-                eq(decor),
             )
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index e40034b..8cccdb2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -81,6 +81,7 @@
 import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
 import org.junit.After
 import org.mockito.Mockito
@@ -147,6 +148,7 @@
     protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
     protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
     protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
+    protected val mockTilingWindowDecoration = mock<DesktopTilingDecorViewModel>()
     protected val motionEvent = mock<MotionEvent>()
     private val displayLayout = mock<DisplayLayout>()
     private val display = mock<Display>()
@@ -226,6 +228,7 @@
             mock<WindowDecorTaskResourceLoader>(),
             mockRecentsTransitionHandler,
             desktopModeCompatPolicy,
+            mockTilingWindowDecoration,
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 71c821d..c4f70ac2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -120,6 +120,7 @@
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
 import kotlin.jvm.functions.Function1;
+import kotlin.jvm.functions.Function2;
 
 import kotlinx.coroutines.CoroutineScope;
 import kotlinx.coroutines.MainCoroutineDispatcher;
@@ -998,8 +999,8 @@
 
         createMaximizeMenu(decoration);
 
-        verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
-                any(), mOnMaxMenuHoverChangeListener.capture(), any());
+        verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+                mOnMaxMenuHoverChangeListener.capture(), any());
         assertTrue(decoration.isMaximizeMenuActive());
     }
 
@@ -1011,8 +1012,8 @@
                 new FakeMaximizeMenuFactory(menu));
         decoration.setAppHeaderMaximizeButtonHovered(false);
         createMaximizeMenu(decoration);
-        verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
-                any(), mOnMaxMenuHoverChangeListener.capture(), any());
+        verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+                mOnMaxMenuHoverChangeListener.capture(), any());
 
         mOnMaxMenuHoverChangeListener.getValue().invoke(false);
 
@@ -1050,8 +1051,8 @@
         final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
                 new FakeMaximizeMenuFactory(menu));
         createMaximizeMenu(decoration);
-        verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
-                any(), mOnMaxMenuHoverChangeListener.capture(), any());
+        verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+                mOnMaxMenuHoverChangeListener.capture(), any());
 
         mOnMaxMenuHoverChangeListener.getValue().invoke(true);
 
@@ -1065,8 +1066,8 @@
         final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
                 new FakeMaximizeMenuFactory(menu));
         createMaximizeMenu(decoration);
-        verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
-                any(), mOnMaxMenuHoverChangeListener.capture(), any());
+        verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+                mOnMaxMenuHoverChangeListener.capture(), any());
 
         decoration.setAppHeaderMaximizeButtonHovered(true);
 
@@ -1086,7 +1087,6 @@
 
         verify(menu).show(
                 anyBoolean(),
-                anyInt(),
                 /* showImmersiveOption= */ eq(true),
                 anyBoolean(),
                 any(),
@@ -1111,7 +1111,6 @@
 
         verify(menu).show(
                 anyBoolean(),
-                anyInt(),
                 /* showImmersiveOption= */ eq(false),
                 anyBoolean(),
                 any(),
@@ -1136,7 +1135,6 @@
 
         verify(menu).show(
                 anyBoolean(),
-                anyInt(),
                 anyBoolean(),
                 /* showSnapOptions= */ eq(true),
                 any(),
@@ -1161,7 +1159,6 @@
 
         verify(menu).show(
                 anyBoolean(),
-                anyInt(),
                 anyBoolean(),
                 /* showSnapOptions= */ eq(false),
                 any(),
@@ -1766,7 +1763,9 @@
                 @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer,
                 @NonNull DisplayController displayController,
                 @NonNull ActivityManager.RunningTaskInfo taskInfo,
-                @NonNull Context decorWindowContext, @NonNull PointF menuPosition,
+                @NonNull Context decorWindowContext,
+                @NonNull Function2<? super Integer,? super Integer,? extends PointF>
+                    positionSupplier,
                 @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) {
             return mMaximizeMenu;
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt
new file mode 100644
index 0000000..7341e09
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 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.windowdecor
+
+import android.app.ActivityManager
+import android.content.Context
+import android.graphics.Region
+import android.os.Handler
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.util.Size
+import android.view.Choreographer
+import android.view.Display
+import android.view.IWindowSession
+import android.view.InputChannel
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestHandler
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger
+import com.android.wm.shell.util.StubTransaction
+import com.android.wm.shell.windowdecor.DragResizeInputListener.TaskResizeInputEventReceiver
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import java.util.function.Supplier
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [DragResizeInputListener].
+ *
+ * Build/Install/Run:
+ *   atest WMShellUnitTests:DragResizeInputListenerTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DragResizeInputListenerTest : ShellTestCase() {
+    private val testMainExecutor = TestShellExecutor()
+    private val testBgExecutor = TestShellExecutor()
+    private val mockWindowSession = mock<IWindowSession>()
+    private val mockInputEventReceiver = mock<TaskResizeInputEventReceiver>()
+
+    @Test
+    fun testGrantInputChannelOffMainThread() {
+        create()
+        testMainExecutor.flushAll()
+
+        verifyNoInputChannelGrantRequests()
+    }
+
+    @Test
+    fun testInitializationCallback_waitsForBgSetup() {
+        val inputListener = create()
+
+        val callback = TestInitializationCallback()
+        inputListener.addInitializedCallback(callback)
+        assertThat(callback.initialized).isFalse()
+
+        testBgExecutor.flushAll()
+        testMainExecutor.flushAll()
+
+        assertThat(callback.initialized).isTrue()
+    }
+
+    @Test
+    fun testInitializationCallback_alreadyInitialized_callsBackImmediately() {
+        val inputListener = create()
+        testBgExecutor.flushAll()
+        testMainExecutor.flushAll()
+
+        val callback = TestInitializationCallback()
+        inputListener.addInitializedCallback(callback)
+
+        assertThat(callback.initialized).isTrue()
+    }
+
+    @Test
+    fun testClose_beforeBgSetup_cancelsBgSetup() {
+        val inputListener = create()
+
+        inputListener.close()
+        testBgExecutor.flushAll()
+
+        verifyNoInputChannelGrantRequests()
+    }
+
+    @Test
+    fun testClose_beforeBgSetupResultSet_cancelsInit() {
+        val inputListener = create()
+        val callback = TestInitializationCallback()
+        inputListener.addInitializedCallback(callback)
+
+        testBgExecutor.flushAll()
+        inputListener.close()
+        testMainExecutor.flushAll()
+
+        assertThat(callback.initialized).isFalse()
+    }
+
+    @Test
+    fun testClose_afterInit_disposesOfReceiver() {
+        val inputListener = create()
+
+        testBgExecutor.flushAll()
+        testMainExecutor.flushAll()
+        inputListener.close()
+
+        verify(mockInputEventReceiver).dispose()
+    }
+
+    @Test
+    fun testClose_afterInit_removesTokens() {
+        val inputListener = create()
+
+        inputListener.close()
+        testBgExecutor.flushAll()
+
+        verify(mockWindowSession).remove(inputListener.mClientToken)
+        verify(mockWindowSession).remove(inputListener.mSinkClientToken)
+    }
+
+    private fun verifyNoInputChannelGrantRequests() {
+        verify(mockWindowSession, never())
+            .grantInputChannel(
+                anyInt(),
+                any(),
+                any(),
+                anyOrNull(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyOrNull(),
+                any(),
+                any(),
+                any(),
+            )
+    }
+
+    private fun create(): DragResizeInputListener =
+        DragResizeInputListener(
+            context,
+            mockWindowSession,
+            testMainExecutor,
+            testBgExecutor,
+            TestTaskResizeInputEventReceiverFactory(mockInputEventReceiver),
+            TestRunningTaskInfoBuilder().build(),
+            TestHandler(Looper.getMainLooper()),
+            mock<Choreographer>(),
+            Display.DEFAULT_DISPLAY,
+            mock<SurfaceControl>(),
+            mock<DragPositioningCallback>(),
+            { SurfaceControl.Builder() },
+            { StubTransaction() },
+            mock<DisplayController>(),
+            mock<DesktopModeEventLogger>(),
+        )
+
+    private class TestInitializationCallback : Runnable {
+        var initialized: Boolean = false
+            private set
+
+        override fun run() {
+            initialized = true
+        }
+    }
+
+    private class TestTaskResizeInputEventReceiverFactory(
+        private val mockInputEventReceiver: TaskResizeInputEventReceiver
+    ) : DragResizeInputListener.TaskResizeInputEventReceiverFactory {
+        override fun create(
+            context: Context,
+            taskInfo: ActivityManager.RunningTaskInfo,
+            inputChannel: InputChannel,
+            callback: DragPositioningCallback,
+            handler: Handler,
+            choreographer: Choreographer,
+            displayLayoutSizeSupplier: Supplier<Size?>,
+            touchRegionConsumer: Consumer<Region?>,
+            desktopModeEventLogger: DesktopModeEventLogger,
+        ): TaskResizeInputEventReceiver = mockInputEventReceiver
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index aa1f82e..af01623 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -40,6 +40,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
@@ -386,6 +387,49 @@
         verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any(), any());
     }
 
+
+    @Test
+    public void testReinflateViewsOnFontScaleChange() {
+        final Display defaultDisplay = mock(Display.class);
+        doReturn(defaultDisplay).when(mMockDisplayController)
+                .getDisplay(Display.DEFAULT_DISPLAY);
+
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setVisible(true)
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .build();
+        final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+        windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+        clearInvocations(windowDecor);
+        final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+                .setVisible(true)
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .build();
+        taskInfo2.configuration.fontScale = taskInfo.configuration.fontScale + 1;
+        windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain());
+        // WindowDecoration#releaseViews should be called since the font scale has changed.
+        verify(windowDecor).releaseViews(any());
+    }
+
+    @Test
+    public void testViewNotReinflatedWhenFontScaleNotChanged() {
+        final Display defaultDisplay = mock(Display.class);
+        doReturn(defaultDisplay).when(mMockDisplayController)
+                .getDisplay(Display.DEFAULT_DISPLAY);
+
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setVisible(true)
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .build();
+        final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+        windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+        clearInvocations(windowDecor);
+        windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+        // WindowDecoration#releaseViews should be called since task info (and therefore the
+        // fontScale) has not changed.
+        verify(windowDecor, never()).releaseViews(any());
+    }
+
     @Test
     public void testAddViewHostViewContainer() {
         final Display defaultDisplay = mock(Display.class);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
index d99a482..c86730e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
@@ -20,6 +20,7 @@
 import android.view.SurfaceControl
 import android.view.View
 import android.view.WindowManager
+import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
 import com.google.common.truth.Truth.assertThat
@@ -30,6 +31,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.never
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 
@@ -47,24 +51,46 @@
     fun update_differentView_replacesView() = runTest {
         val view = View(context)
         val lp = WindowManager.LayoutParams()
-        val reusableVH = createReusableViewHost()
-        reusableVH.updateView(view, lp, context.resources.configuration, null)
+        val rootView = FrameLayout(context)
+        val reusableVH = createReusableViewHost(rootView)
+        reusableVH.updateView(view, lp, context.resources.configuration)
 
-        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
+        assertThat(rootView.childCount).isEqualTo(1)
+        assertThat(rootView.getChildAt(0)).isEqualTo(view)
 
         val newView = View(context)
         val newLp = WindowManager.LayoutParams()
-        reusableVH.updateView(newView, newLp, context.resources.configuration, null)
+        reusableVH.updateView(newView, newLp, context.resources.configuration)
 
-        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
+        assertThat(rootView.childCount).isEqualTo(1)
+        assertThat(rootView.getChildAt(0)).isEqualTo(newView)
+    }
+
+    @Test
+    fun update_sameView_doesNotReplaceView() = runTest {
+        val view = View(context)
+        val lp = WindowManager.LayoutParams()
+        val spyRootView = spy(FrameLayout(context))
+        val reusableVH = createReusableViewHost(spyRootView)
+        reusableVH.updateView(view, lp, context.resources.configuration)
+
+        verify(spyRootView, times(1)).removeAllViews()
+        assertThat(spyRootView.childCount).isEqualTo(1)
+        assertThat(spyRootView.getChildAt(0)).isEqualTo(view)
+
+        reusableVH.updateView(view, lp, context.resources.configuration)
+
+        clearInvocations(spyRootView)
+        verify(spyRootView, never()).removeAllViews()
+        assertThat(spyRootView.childCount).isEqualTo(1)
+        assertThat(spyRootView.getChildAt(0)).isEqualTo(view)
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun updateView_clearsPendingAsyncJob() = runTest {
-        val reusableVH = createReusableViewHost()
+        val rootView = FrameLayout(context)
+        val reusableVH = createReusableViewHost(rootView)
         val asyncView = View(context)
         val syncView = View(context)
         val asyncAttrs = WindowManager.LayoutParams(100, 100)
@@ -83,7 +109,6 @@
             view = syncView,
             attrs = syncAttrs,
             configuration = context.resources.configuration,
-            onDrawTransaction = null,
         )
 
         // Would run coroutine if it hadn't been cancelled.
@@ -91,7 +116,7 @@
 
         assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
         // View host view/attrs should match the ones from the sync call.
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
+        assertThat(rootView.getChildAt(0)).isEqualTo(syncView)
         assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
     }
 
@@ -118,7 +143,8 @@
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun updateViewAsync_clearsPendingAsyncJob() = runTest {
-        val reusableVH = createReusableViewHost()
+        val rootView = FrameLayout(context)
+        val reusableVH = createReusableViewHost(rootView)
 
         val view = View(context)
         reusableVH.updateViewAsync(
@@ -136,7 +162,7 @@
         advanceUntilIdle()
 
         assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
+        assertThat(rootView.getChildAt(0)).isEqualTo(otherView)
     }
 
     @Test
@@ -148,7 +174,6 @@
             view = view,
             attrs = WindowManager.LayoutParams(100, 100),
             configuration = context.resources.configuration,
-            onDrawTransaction = null,
         )
 
         val t = mock(SurfaceControl.Transaction::class.java)
@@ -159,19 +184,23 @@
 
     @Test
     fun warmUp_addsRootView() = runTest {
-        val reusableVH = createReusableViewHost().apply { warmUp() }
+        val rootView = FrameLayout(context)
+        val reusableVH = createReusableViewHost(rootView).apply { warmUp() }
 
         assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView)
+        assertThat(reusableVH.view()).isEqualTo(rootView)
     }
 
-    private fun CoroutineScope.createReusableViewHost() =
+    private fun CoroutineScope.createReusableViewHost(
+        rootView: FrameLayout = FrameLayout(context)
+    ) =
         ReusableWindowDecorViewHost(
             context = context,
             mainScope = this,
             display = context.display,
             id = 1,
             viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+            rootView
         )
 
     private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 0fa31c7..f5e10d9 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -1467,8 +1467,6 @@
   }
 
   const StringPiece16 kAttr16 = u"attr";
-  const static std::u16string kAttrPrivate16 = u"^attr-private";
-
   for (const PackageGroup& package_group : package_groups_) {
     for (const ConfiguredPackage& package_impl : package_group.packages_) {
       const LoadedPackage* package = package_impl.loaded_package_;
@@ -1480,12 +1478,13 @@
       base::expected<uint32_t, NullOrIOError> resid = package->FindEntryByName(type16, entry16);
       if (UNLIKELY(IsIOError(resid))) {
          return base::unexpected(resid.error());
-       }
+      }
 
       if (!resid.has_value() && kAttr16 == type16) {
         // Private attributes in libraries (such as the framework) are sometimes encoded
         // under the type '^attr-private' in order to leave the ID space of public 'attr'
         // free for future additions. Check '^attr-private' for the same name.
+        const static std::u16string kAttrPrivate16 = u"^attr-private";
         resid = package->FindEntryByName(kAttrPrivate16, entry16);
       }
 
diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp
index ea9e9a2..9aacdcb 100644
--- a/libs/androidfw/LocaleDataLookup.cpp
+++ b/libs/androidfw/LocaleDataLookup.cpp
@@ -14871,12 +14871,22 @@
         case 0x656E4154u: // en-AT -> en-150
         case 0x656E4245u: // en-BE -> en-150
         case 0x656E4348u: // en-CH -> en-150
+        case 0x656E435Au: // en-CZ -> en-150
         case 0x656E4445u: // en-DE -> en-150
         case 0x656E444Bu: // en-DK -> en-150
+        case 0x656E4553u: // en-ES -> en-150
         case 0x656E4649u: // en-FI -> en-150
+        case 0x656E4652u: // en-FR -> en-150
+        case 0x656E4855u: // en-HU -> en-150
+        case 0x656E4954u: // en-IT -> en-150
         case 0x656E4E4Cu: // en-NL -> en-150
+        case 0x656E4E4Fu: // en-NO -> en-150
+        case 0x656E504Cu: // en-PL -> en-150
+        case 0x656E5054u: // en-PT -> en-150
+        case 0x656E524Fu: // en-RO -> en-150
         case 0x656E5345u: // en-SE -> en-150
         case 0x656E5349u: // en-SI -> en-150
+        case 0x656E534Bu: // en-SK -> en-150
             return 0x656E80A1u;
         case 0x65734152u: // es-AR -> es-419
         case 0x6573424Fu: // es-BO -> es-419
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 62fd7d3..d3fc91b 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -137,6 +137,14 @@
 }
 
 flag {
+  name: "shader_color_space"
+  is_exported: true
+  namespace: "core_graphics"
+  description: "API to set the working colorspace of a Shader or ColorFilter"
+  bug: "299670828"
+}
+
+flag {
   name: "query_global_priority"
   namespace: "core_graphics"
   description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation."
@@ -174,7 +182,7 @@
 flag {
   name: "early_preload_gl_context"
   namespace: "core_graphics"
-  description: "Initialize GL context and GraphicBufferAllocater init on renderThread preload. This improves app startup time for apps using GL."
+  description: "Preload GL context on renderThread preload. This improves app startup time for apps using GL."
   bug: "383612849"
 }
 
@@ -187,4 +195,12 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
+}
+
+flag {
+  name: "early_preinit_buffer_allocator"
+  namespace: "core_graphics"
+  description: "Initialize GraphicBufferAllocater on ViewRootImpl init, to avoid blocking on init during buffer allocation, improving app launch latency."
+  bug: "389908734"
+  is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index eadb9de..45f0fe0 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -266,11 +266,17 @@
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete));
 }
 
-static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) {
+static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr,
+                                  jlong colorSpacePtr) {
     SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder);
     const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr);
+    auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr);
     sk_sp<SkShader> shader = builder->makeShader(matrix);
     ThrowIAE_IfNull(env, shader);
+    if (colorSpace) {
+        shader = shader->makeWithWorkingColorSpace(colorSpace);
+        ThrowIAE_IfNull(env, shader);
+    }
     return reinterpret_cast<jlong>(shader.release());
 }
 
@@ -350,6 +356,10 @@
     UpdateChild(env, builder, name.c_str(), childEffect);
 }
 
+static void RuntimeShader_no(JNIEnv* env) {
+    jniThrowRuntimeException(env, "Not supported");
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////
 
 static const JNINativeMethod gShaderMethods[] = {
@@ -379,7 +389,8 @@
 
 static const JNINativeMethod gRuntimeShaderMethods[] = {
         {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer},
-        {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create},
+        {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_no},
+        {"nativeCreateShader", "(JJJ)J", (void*)RuntimeShader_create},
         {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder},
         {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
          (void*)RuntimeShader_updateFloatArrayUniforms},
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index df9f830..99e7740 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -52,6 +52,9 @@
 #include <renderthread/RenderThread.h>
 #include <src/image/SkImage_Base.h>
 #include <thread/CommonPool.h>
+#ifdef __ANDROID__
+#include <ui/GraphicBufferAllocator.h>
+#endif
 #include <utils/Color.h>
 #include <utils/RefBase.h>
 #include <utils/StrongPointer.h>
@@ -849,6 +852,17 @@
     RenderProxy::preload();
 }
 
+static void android_view_ThreadedRenderer_preInitBufferAllocator(JNIEnv*, jclass) {
+#ifdef __ANDROID__
+    CommonPool::async([] {
+        ATRACE_NAME("preInitBufferAllocator:GraphicBufferAllocator");
+        // This involves several binder calls which we do not want blocking
+        // critical path of the activity that is launching.
+        GraphicBufferAllocator::getInstance();
+    });
+#endif
+}
+
 static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jobject clazz,
                                                                  jboolean enabled) {
     RenderProxy::setRtAnimationsEnabled(enabled);
@@ -1040,6 +1054,8 @@
          (void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
         {"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
         {"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
+        {"preInitBufferAllocator", "()V",
+         (void*)android_view_ThreadedRenderer_preInitBufferAllocator},
         {"isWebViewOverlaysEnabled", "()Z",
          (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled},
         {"nSetDrawingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDrawingEnabled},
diff --git a/location/Android.bp b/location/Android.bp
index bc02d1f..80556a2 100644
--- a/location/Android.bp
+++ b/location/Android.bp
@@ -42,6 +42,7 @@
             "FlaggedApi",
         ],
     },
+    jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
 }
 
 platform_compat_config {
diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java
index e1fbfea..892a861 100644
--- a/media/java/android/media/AudioDeviceVolumeManager.java
+++ b/media/java/android/media/AudioDeviceVolumeManager.java
@@ -86,10 +86,10 @@
     /**
      * @hide
      * Interface to receive volume changes on a device that behaves in absolute volume mode.
-     * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor,
-     *         OnAudioDeviceVolumeChangeListener)
-     * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor,
-     *         OnAudioDeviceVolumeChangeListener)
+     * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, boolean, Executor,
+     *          OnAudioDeviceVolumeChangedListener)
+     * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, boolean, Executor,
+     *          OnAudioDeviceVolumeChangedListener)
      */
     public interface OnAudioDeviceVolumeChangedListener {
         /**
@@ -203,6 +203,9 @@
      * volume updates to apply on that device
      * @param device the audio device set to absolute volume mode
      * @param volume the type of volume this device responds to
+     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+     * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+     * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
      * @param executor the Executor used for receiving volume updates through the listener
      * @param vclistener the callback for volume updates
      */
@@ -211,13 +214,13 @@
     public void setDeviceAbsoluteVolumeBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
         final ArrayList<VolumeInfo> volumes = new ArrayList<>(1);
         volumes.add(volume);
-        setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
-                handlesVolumeAdjustment);
+        setDeviceAbsoluteMultiVolumeBehavior(device, volumes, handlesVolumeAdjustment, executor,
+                vclistener);
     }
 
     /**
@@ -226,20 +229,20 @@
      * registers a listener for receiving volume updates to apply on that device
      * @param device the audio device set to absolute multi-volume mode
      * @param volumes the list of volumes the given device responds to
+     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+     * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+     * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
      * @param executor the Executor used for receiving volume updates through the listener
      * @param vclistener the callback for volume updates
-     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
-     *  from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
-     *  will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
      */
     @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING,
             android.Manifest.permission.BLUETOOTH_PRIVILEGED })
     public void setDeviceAbsoluteMultiVolumeBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull List<VolumeInfo> volumes,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
         baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
                 handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
     }
@@ -249,11 +252,14 @@
      * Configures a device to use absolute volume model, and registers a listener for receiving
      * volume updates to apply on that device.
      *
-     * Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no reliable
-     * way to set the device's volume to a percentage.
+     * <p>Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no
+     * reliable way to set the device's volume to a percentage.
      *
      * @param device the audio device set to absolute volume mode
      * @param volume the type of volume this device responds to
+     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+     * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+     * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
      * @param executor the Executor used for receiving volume updates through the listener
      * @param vclistener the callback for volume updates
      */
@@ -262,13 +268,13 @@
     public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
         final ArrayList<VolumeInfo> volumes = new ArrayList<>(1);
         volumes.add(volume);
-        setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, executor, vclistener,
-                handlesVolumeAdjustment);
+        setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, handlesVolumeAdjustment,
+                executor, vclistener);
     }
 
     /**
@@ -276,11 +282,14 @@
      * Configures a device to use absolute volume model applied to different volume types, and
      * registers a listener for receiving volume updates to apply on that device.
      *
-     * Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is
+     * <p>Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is
      * no reliable way to set the device's volume to a percentage.
      *
      * @param device the audio device set to absolute multi-volume mode
      * @param volumes the list of volumes the given device responds to
+     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+     * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+     * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
      * @param executor the Executor used for receiving volume updates through the listener
      * @param vclistener the callback for volume updates
      */
@@ -289,16 +298,16 @@
     public void setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull List<VolumeInfo> volumes,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
         baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
                 handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
     }
 
     /**
      * Base method for configuring a device to use absolute volume behavior, or one of its variants.
-     * See {@link AudioManager#AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors.
+     * See {@link AudioManager.AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors.
      *
      * @param behavior the variant of absolute device volume behavior to adopt
      */
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 12d7f33..e01cb92 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1754,13 +1754,21 @@
     @UnsupportedAppUsage
     public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
             int codecFormat) {
+        return setDeviceConnectionState(attributes, state, codecFormat, false /*deviceSwitch*/);
+    }
+
+    /**
+     * @hide
+     */
+    public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
+            int codecFormat, boolean deviceSwitch) {
         android.media.audio.common.AudioPort port =
                 AidlConversion.api2aidl_AudioDeviceAttributes_AudioPort(attributes);
         Parcel parcel = Parcel.obtain();
         port.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
         try {
-            return setDeviceConnectionState(state, parcel, codecFormat);
+            return setDeviceConnectionState(state, parcel, codecFormat, deviceSwitch);
         } finally {
             parcel.recycle();
         }
@@ -1769,7 +1777,10 @@
      * @hide
      */
     @UnsupportedAppUsage
-    public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat);
+    public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat,
+                                                      boolean deviceSwitch);
+
+
     /** @hide */
     @UnsupportedAppUsage
     public static native int getDeviceConnectionState(int device, String device_address);
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index fb1b5b5..15c8323 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -4060,6 +4060,7 @@
          * Finish building a queue request and queue the buffers with tunings.
          */
         public void queue() {
+            Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueRequest-queue#java");
             if (!isAccessible()) {
                 throw new IllegalStateException("The request is stale");
             }
@@ -4088,6 +4089,7 @@
                         mTuningKeys, mTuningValues);
             }
             clear();
+            Trace.traceEnd(Trace.TRACE_TAG_VIDEO);
         }
 
         @NonNull QueueRequest clear() {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 0f24654..0213481 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -60,6 +60,7 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * RingtoneManager provides access to ringtones, notification, and other types
@@ -810,9 +811,7 @@
         // Don't set the stream type
         Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
                 volumeShaperConfig, false);
-        if (Flags.enableRingtoneHapticsCustomization()
-                && Utils.isRingtoneVibrationSettingsSupported(context)
-                && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) {
+        if (muteHapticChannelForVibration(context, ringtoneUri)) {
             audioAttributes = new AudioAttributes.Builder(
                     audioAttributes).setHapticChannelsMuted(true).build();
         }
@@ -1305,4 +1304,19 @@
             default: throw new IllegalArgumentException();
         }
     }
+
+    private static boolean muteHapticChannelForVibration(Context context, Uri ringtoneUri) {
+        final Uri vibrationUri = Utils.getVibrationUri(ringtoneUri);
+        // No vibration is specified
+        if (vibrationUri == null) {
+            return false;
+        }
+        // The user specified the synchronized pattern
+        if (Objects.equals(vibrationUri.toString(), Utils.SYNCHRONIZED_VIBRATION)) {
+            return false;
+        }
+        return Flags.enableRingtoneHapticsCustomization()
+                && Utils.isRingtoneVibrationSettingsSupported(context)
+                && hasHapticChannels(ringtoneUri);
+    }
 }
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index 11bd221..d6e27b0 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -66,6 +66,8 @@
 
     public static final String VIBRATION_URI_PARAM = "vibration_uri";
 
+    public static final String SYNCHRONIZED_VIBRATION = "synchronized";
+
     /**
      * Sorts distinct (non-intersecting) range array in ascending order.
      * @throws java.lang.IllegalArgumentException if ranges are not distinct
@@ -757,8 +759,8 @@
             return null;
         }
         String filePath = vibrationUri.getPath();
-        if (filePath == null) {
-            Log.w(TAG, "The file path is null.");
+        if (filePath == null || filePath.equals(Utils.SYNCHRONIZED_VIBRATION)) {
+            Log.w(TAG, "Ignore the vibration parsing for file:" + filePath);
             return null;
         }
         File vibrationFile = new File(filePath);
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
index a3b06e8..5590ca72 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
@@ -23,6 +23,7 @@
 import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
 import android.app.backup.BackupTransport;
 import android.app.backup.RestoreDescription;
@@ -38,6 +39,7 @@
 import android.system.StructStat;
 import android.util.ArrayMap;
 import android.util.Base64;
+import android.util.Dumpable;
 import android.util.Log;
 
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
@@ -51,6 +53,8 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -59,11 +63,14 @@
 /**
  * Backup transport for stashing stuff into a known location on disk, and
  * later restoring from there.  For testing only.
+ *
+ * <p>Note: the quickest way to build and sync this class is:
+ * {@code m LocalTransport && adb install $OUT/system/priv-app/LocalTransport/LocalTransport.apk}
  */
 
 public class LocalTransport extends BackupTransport {
-    private static final String TAG = "LocalTransport";
-    private static final boolean DEBUG = false;
+    static final String TAG = "LocalTransport";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
 
     private static final String TRANSPORT_DIR_NAME
             = "com.android.localtransport.LocalTransport";
@@ -124,6 +131,8 @@
     }
 
     public LocalTransport(Context context, LocalTransportParameters parameters) {
+        Log.i(TAG, "Creating LocalTransport for user " + context.getUserId()
+                + " (DEBUG=" + DEBUG + ")");
         mContext = context;
         mParameters = parameters;
         makeDataDirs();
@@ -910,35 +919,68 @@
         return mMonitor;
     }
 
-    private class TestBackupManagerMonitor extends BackupManagerMonitor {
+    private class TestBackupManagerMonitor extends BackupManagerMonitor implements Dumpable {
+
+        private final List<DataTypeResult> mReceivedResults = new ArrayList<>();
+        private int mNumberReceivedEvents;
+
         @Override
         public void onEvent(Bundle event) {
-            if (event == null || !mParameters.logAgentResults()) {
+            if (event == null) {
+                if (DEBUG) {
+                    Log.w(TAG, "onEvent() called with null");
+                }
                 return;
             }
-
-            if (event.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID)
-                    == BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS) {
-                Log.i(TAG, "agent_logging_results {");
-                ArrayList<DataTypeResult> results = event.getParcelableArrayList(
-                        BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS,
-                        DataTypeResult.class);
-                for (DataTypeResult result : results) {
-                    Log.i(TAG, "\tdataType: " + result.getDataType());
-                    Log.i(TAG, "\tsuccessCount: " + result.getSuccessCount());
-                    Log.i(TAG, "\tfailCount: " + result.getFailCount());
-                    Log.i(TAG, "\tmetadataHash: " + Arrays.toString(result.getMetadataHash()));
-
-                    if (!result.getErrors().isEmpty()) {
-                        Log.i(TAG, "\terrors {");
-                        for (String error : result.getErrors().keySet()) {
-                            Log.i(TAG, "\t\t" + error + ": " + result.getErrors().get(error));
-                        }
-                        Log.i(TAG, "\t}");
-                    }
-
-                    Log.i(TAG, "}");
+            int eventId = event.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID);
+            if (eventId != BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS) {
+                if (DEBUG) Log.v(TAG, "ignoring event with id " + eventId);
+                return;
+            }
+            mNumberReceivedEvents++;
+            boolean logResults = mParameters.logAgentResults();
+            ArrayList<DataTypeResult> results = event.getParcelableArrayList(
+                    BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS,
+                    DataTypeResult.class);
+            int size = results.size();
+            if (size == 0) {
+                if (logResults) {
+                    Log.i(TAG, "no agent_logging_results on event #" + mNumberReceivedEvents);
                 }
+                return;
+            }
+            if (logResults) {
+                Log.i(TAG, "agent_logging_results {");
+            }
+            for (int i = 0; i < size; i++) {
+                var result = results.get(i);
+                mReceivedResults.add(result);
+                if (logResults) {
+                    Log.i(TAG, "\t" + BackupRestoreEventLogger.toString(result));
+                }
+            }
+            if (logResults) {
+                Log.i(TAG, "}");
+            }
+        }
+
+        @Override
+        public void dump(PrintWriter pw, String[] args) {
+            pw.println("TestBackupManagerMonitor");
+            pw.printf("%d events received", mNumberReceivedEvents);
+            if (mNumberReceivedEvents == 0) {
+                pw.println();
+                return;
+            }
+            int size = mReceivedResults.size();
+            if (size == 0) {
+                pw.println("; no results on them");
+                return;
+            }
+            pw.printf("; %d results on them:\n", size);
+            for (int i = 0; i < size; i++) {
+                DataTypeResult result = mReceivedResults.get(i);
+                pw.printf("  #%d: %s\n", i, BackupRestoreEventLogger.toString(result));
             }
         }
     }
@@ -953,4 +995,60 @@
         }
         return mParameters.noRestrictedModePackages();
     }
+
+    @Override
+    public String toString() {
+        try {
+            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
+                dump(pw, /* args= */ null);
+                pw.flush();
+                return sw.toString();
+            }
+        } catch (IOException e) {
+            // Shouldn't happen...
+            Log.e(TAG, "toString(): failed to dump", e);
+            return super.toString();
+        }
+    }
+
+    void dump(PrintWriter pw, String[] args) {
+        pw.printf("mDataDir: %s\n", mDataDir);
+        pw.printf("mCurrentSetDir: %s\n", mCurrentSetDir);
+        pw.printf("mCurrentSetIncrementalDir: %s\n", mCurrentSetIncrementalDir);
+        pw.printf("mCurrentSetFullDir: %s\n", mCurrentSetFullDir);
+
+        pw.printf("mRestorePackages: %s\n", Arrays.toString(mRestorePackages));
+        pw.printf("mRestorePackage: %d\n", mRestorePackage);
+        pw.printf("mRestoreType: %d\n", mRestoreType);
+        pw.printf("mRestoreSetDir: %s\n", mRestoreSetDir);
+        pw.printf("mRestoreSetIncrementalDir: %s\n", mRestoreSetIncrementalDir);
+        pw.printf("mRestoreSetFullDir: %s\n", mRestoreSetFullDir);
+
+        pw.printf("mFullTargetPackage: %s\n", mFullTargetPackage);
+        pw.printf("mSocket: %s\n", mSocket);
+        pw.printf("mSocketInputStream: %s\n", mSocketInputStream);
+        pw.printf("mFullBackupOutputStream: %s\n", mFullBackupOutputStream);
+        dumpByteArray(pw, "mFullBackupBuffer", mFullBackupBuffer);
+        pw.printf("mFullBackupSize: %d\n", mFullBackupSize);
+
+        pw.printf("mCurFullRestoreStream: %s\n", mCurFullRestoreStream);
+        dumpByteArray(pw, "mFullRestoreBuffer", mFullRestoreBuffer);
+        if (mParameters == null) {
+            pw.println("No LocalTransportParameters");
+        } else {
+            pw.println(mParameters);
+        }
+        if (mMonitor instanceof Dumpable) {
+            ((Dumpable) mMonitor).dump(pw, args);
+        }
+
+        pw.printf("currentDestinationString(): %s\n", currentDestinationString());
+        pw.printf("dataManagementIntent(): %s\n", dataManagementIntent());
+        pw.printf("dataManagementIntentLabel(): %s\n", dataManagementIntentLabel());
+        pw.printf("transportDirName(): %s\n", transportDirName());
+    }
+
+    private void dumpByteArray(PrintWriter pw, String name, byte[] array) {
+        pw.printf("%s size: %d\n", name, (array == null ? 0 : array.length));
+    }
 }
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
index c980913..c7cfc5b 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
@@ -16,11 +16,15 @@
 
 package com.android.localtransport;
 
+import static com.android.localtransport.LocalTransport.DEBUG;
+import static com.android.localtransport.LocalTransport.TAG;
+
 import android.content.ContentResolver;
 import android.os.Handler;
 import android.provider.Settings;
 import android.util.KeyValueListParser;
 import android.util.KeyValueSettingObserver;
+import android.util.Log;
 
 import java.util.Arrays;
 import java.util.List;
@@ -76,15 +80,30 @@
     }
 
     public String getSettingValue(ContentResolver resolver) {
-        return Settings.Secure.getString(resolver, SETTING);
+        String value = Settings.Secure.getString(resolver, SETTING);
+        if (DEBUG) {
+            Log.d(TAG, "LocalTransportParameters.getSettingValue(): returning " + value);
+        }
+        return value;
     }
 
     public void update(KeyValueListParser parser) {
-        mFakeEncryptionFlag = parser.getBoolean(KEY_FAKE_ENCRYPTION_FLAG, false);
-        mIsNonIncrementalOnly = parser.getBoolean(KEY_NON_INCREMENTAL_ONLY, false);
-        mIsDeviceTransfer = parser.getBoolean(KEY_IS_DEVICE_TRANSFER, false);
-        mIsEncrypted = parser.getBoolean(KEY_IS_ENCRYPTED, false);
-        mLogAgentResults = parser.getBoolean(KEY_LOG_AGENT_RESULTS, /* def */ false);
+        mFakeEncryptionFlag = parser.getBoolean(KEY_FAKE_ENCRYPTION_FLAG, /* def= */ false);
+        mIsNonIncrementalOnly = parser.getBoolean(KEY_NON_INCREMENTAL_ONLY, /* def= */ false);
+        mIsDeviceTransfer = parser.getBoolean(KEY_IS_DEVICE_TRANSFER, /* def= */ false);
+        mIsEncrypted = parser.getBoolean(KEY_IS_ENCRYPTED, /* def= */ false);
+        mLogAgentResults = parser.getBoolean(KEY_LOG_AGENT_RESULTS, /* def= */ false);
         mNoRestrictedModePackages = parser.getString(KEY_NO_RESTRICTED_MODE_PACKAGES, /* def */ "");
     }
+
+    @Override
+    public String toString() {
+        return "LocalTransportParameters[mFakeEncryptionFlag=" + mFakeEncryptionFlag
+                + ", mIsNonIncrementalOnly=" + mIsNonIncrementalOnly
+                + ", mIsDeviceTransfer=" + mIsDeviceTransfer
+                + ", mIsEncrypted=" + mIsEncrypted
+                + ", mLogAgentResults=" + mLogAgentResults
+                + ", noRestrictedModePackages()=" + noRestrictedModePackages()
+                + "]";
+    }
 }
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransportService.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransportService.java
index ac4f418..2f6c57a 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransportService.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransportService.java
@@ -16,15 +16,28 @@
 
 package com.android.localtransport;
 
+import static com.android.localtransport.LocalTransport.DEBUG;
+import static com.android.localtransport.LocalTransport.TAG;
+
+import android.annotation.Nullable;
 import android.app.Service;
 import android.content.Intent;
 import android.os.IBinder;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 
 public class LocalTransportService extends Service {
-    private static LocalTransport sTransport = null;
+
+    @Nullable
+    private static LocalTransport sTransport;
 
     @Override
     public void onCreate() {
+        if (DEBUG) {
+            Log.d(TAG, "LocalTransportService.onCreate()");
+        }
         if (sTransport == null) {
             LocalTransportParameters parameters =
                     new LocalTransportParameters(getMainThreadHandler(), getContentResolver());
@@ -35,11 +48,27 @@
 
     @Override
     public void onDestroy() {
+        if (DEBUG) {
+            Log.d(TAG, "LocalTransportService.onDestroy()");
+        }
         sTransport.getParameters().stop();
     }
 
     @Override
     public IBinder onBind(Intent intent) {
+        if (DEBUG) {
+            Log.d(TAG, "LocalTransportService.onBind(" + intent + "): parameters="
+                    + sTransport.getParameters());
+        }
         return sTransport.getBinder();
     }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (sTransport == null) {
+            pw.println("No sTransport");
+            return;
+        }
+        sTransport.dump(pw, args);
+    }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index c61a2ac..481023e 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -16,9 +16,6 @@
 
 package com.android.packageinstaller.v2.ui
 
-import android.app.Activity.RESULT_CANCELED
-import android.app.Activity.RESULT_FIRST_USER
-import android.app.Activity.RESULT_OK
 import android.app.AppOpsManager
 import android.content.ActivityNotFoundException
 import android.content.Intent
@@ -67,6 +64,7 @@
             InstallLaunch::class.java.packageName + ".callingPkgName"
         private val LOG_TAG = InstallLaunch::class.java.simpleName
         private const val TAG_DIALOG = "dialog"
+        private const val ARGS_SAVED_INTENT = "saved_intent"
     }
 
     /**
@@ -96,7 +94,15 @@
             intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
             intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID)
         )
-        installViewModel!!.preprocessIntent(intent, info)
+
+        var savedIntent: Intent? = null
+        if (savedInstanceState != null) {
+            savedIntent = savedInstanceState.getParcelable(ARGS_SAVED_INTENT, Intent::class.java)
+        }
+        if (!intent.filterEquals(savedIntent)) {
+            installViewModel!!.preprocessIntent(intent, info)
+        }
+
         installViewModel!!.currentInstallStage.observe(this) { installStage: InstallStage ->
             onInstallStageChange(installStage)
         }
@@ -344,6 +350,11 @@
         appOpsManager!!.stopWatchingMode(listener)
     }
 
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putParcelable(ARGS_SAVED_INTENT, intent)
+        super.onSaveInstanceState(outState)
+    }
+
     override fun onDestroy() {
         super.onDestroy()
         while (activeUnknownSourcesListeners.isNotEmpty()) {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt
index c4ca272..0a02845e 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt
@@ -16,7 +16,6 @@
 
 package com.android.packageinstaller.v2.ui
 
-import android.app.Activity
 import android.app.NotificationManager
 import android.content.Intent
 import android.os.Bundle
@@ -51,6 +50,7 @@
             UninstallLaunch::class.java.packageName + ".callingActivityName"
         val LOG_TAG = UninstallLaunch::class.java.simpleName
         private const val TAG_DIALOG = "dialog"
+        private const val ARGS_SAVED_INTENT = "saved_intent"
     }
 
     private var uninstallViewModel: UninstallViewModel? = null
@@ -76,7 +76,15 @@
             intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME),
             intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID)
         )
-        uninstallViewModel!!.preprocessIntent(intent, callerInfo)
+
+        var savedIntent: Intent? = null
+        if (savedInstanceState != null) {
+            savedIntent = savedInstanceState.getParcelable(ARGS_SAVED_INTENT, Intent::class.java)
+        }
+        if (!intent.filterEquals(savedIntent)) {
+            uninstallViewModel!!.preprocessIntent(intent, callerInfo)
+        }
+
         uninstallViewModel!!.currentUninstallStage.observe(this) { uninstallStage: UninstallStage ->
             onUninstallStageChange(uninstallStage)
         }
@@ -171,6 +179,11 @@
             Log.d(LOG_TAG, "Cancelling uninstall")
         }
         uninstallViewModel!!.cancelUninstall()
-        setResult(Activity.RESULT_FIRST_USER, null, true)
+        setResult(RESULT_FIRST_USER, null, true)
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putParcelable(ARGS_SAVED_INTENT, intent)
+        super.onSaveInstanceState(outState)
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
index 388e03f..5a2b122 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
@@ -49,6 +49,20 @@
                 _currentInstallStage.value = installStage
             }
         }
+
+        // Since staging is an async operation, we will get the staging result later in time.
+        // Result of the file staging will be set in InstallRepository#mStagingResult.
+        // As such, mCurrentInstallStage will need to add another MutableLiveData
+        // as a data source
+        _currentInstallStage.addSource(
+            repository.stagingResult.distinctUntilChanged()
+        ) { installStage: InstallStage ->
+            if (installStage.stageCode != InstallStage.STAGE_READY) {
+                _currentInstallStage.value = installStage
+            } else {
+                checkIfAllowedAndInitiateInstall()
+            }
+        }
     }
 
     fun preprocessIntent(intent: Intent, callerInfo: InstallRepository.CallerInfo) {
@@ -56,18 +70,7 @@
         if (stage.stageCode == InstallStage.STAGE_ABORTED) {
             _currentInstallStage.value = stage
         } else {
-            // Since staging is an async operation, we will get the staging result later in time.
-            // Result of the file staging will be set in InstallRepository#mStagingResult.
-            // As such, mCurrentInstallStage will need to add another MutableLiveData
-            // as a data source
             repository.stageForInstall()
-            _currentInstallStage.addSource(repository.stagingResult) { installStage: InstallStage ->
-                if (installStage.stageCode != InstallStage.STAGE_READY) {
-                    _currentInstallStage.value = installStage
-                } else {
-                    checkIfAllowedAndInitiateInstall()
-                }
-            }
         }
     }
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
index b56b944..af8e856 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
@@ -35,5 +35,6 @@
         "com.android.extservices",
         "com.android.permission",
         "com.android.healthfitness",
+        "com.android.mediaprovider",
     ],
 }
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
index bfaeb42..8d12f01 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
@@ -17,7 +17,9 @@
 package com.android.settingslib.widget
 
 import android.os.Bundle
+import android.view.LayoutInflater;
 import android.view.View
+import android.view.ViewGroup;
 import androidx.annotation.CallSuper
 import androidx.preference.PreferenceFragmentCompat
 import androidx.preference.PreferenceScreen
@@ -27,6 +29,15 @@
 abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() {
 
     @CallSuper
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    @CallSuper
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         if (SettingsThemeHelper.isExpressiveTheme(requireContext())) {
diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp
index e04af6c..6b9cbfa 100644
--- a/packages/SettingsLib/SettingsTransition/Android.bp
+++ b/packages/SettingsLib/SettingsTransition/Android.bp
@@ -30,5 +30,6 @@
         "com.android.extservices",
         "com.android.permission",
         "com.android.healthfitness",
+        "com.android.mediaprovider",
     ],
 }
diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp
index 76e36dc..bf26264 100644
--- a/packages/SettingsLib/TopIntroPreference/Android.bp
+++ b/packages/SettingsLib/TopIntroPreference/Android.bp
@@ -32,5 +32,6 @@
         "com.android.cellbroadcast",
         "com.android.devicelock",
         "com.android.healthfitness",
+        "com.android.mediaprovider",
     ],
 }
diff --git a/packages/SettingsLib/res/drawable/ic_1x_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_1x_mobiledata_updated.xml
new file mode 100644
index 0000000..bd2fad2
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_1x_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="12.88dp"
+    android:viewportWidth="13.53"
+    android:viewportHeight="10.25">
+    <path
+        android:pathData="M3.738,10.252C3.468,10.252 3.234,10.154 3.038,9.958C2.847,9.757 2.751,9.522 2.751,9.251L2.751,2.587L1.372,3.497C1.167,3.628 0.943,3.672 0.7,3.63C0.462,3.588 0.276,3.467 0.14,3.266C0.01,3.065 -0.032,2.844 0.014,2.601C0.066,2.358 0.192,2.172 0.392,2.041L3.185,0.2C3.265,0.149 3.344,0.104 3.423,0.067C3.507,0.025 3.619,0.004 3.759,0.004C4.03,0.004 4.259,0.102 4.445,0.298C4.637,0.494 4.732,0.734 4.732,1.019L4.732,9.251C4.732,9.522 4.634,9.757 4.438,9.958C4.242,10.154 4.009,10.252 3.738,10.252ZM12.582,10.245C12.391,10.245 12.218,10.194 12.064,10.091C11.915,9.984 11.796,9.848 11.707,9.685L9.803,6.038L9.593,5.961L7.332,1.411C7.136,1.084 7.127,0.769 7.304,0.466C7.482,0.163 7.755,0.011 8.123,0.011C8.301,0.011 8.466,0.065 8.62,0.172C8.779,0.279 8.903,0.415 8.991,0.578L10.783,4.015L11.014,4.05L13.373,8.796C13.569,9.132 13.581,9.459 13.408,9.776C13.236,10.089 12.96,10.245 12.582,10.245ZM7.92,10.238C7.57,10.238 7.309,10.089 7.136,9.79C6.968,9.491 6.978,9.186 7.164,8.873L9.621,4.008L9.817,4.008L11.63,0.557C11.719,0.398 11.836,0.27 11.98,0.172C12.125,0.069 12.288,0.018 12.47,0.018C12.816,0.018 13.075,0.163 13.247,0.452C13.42,0.741 13.411,1.047 13.219,1.369L10.909,5.982L10.762,5.989L8.781,9.713C8.697,9.867 8.578,9.993 8.424,10.091C8.27,10.189 8.102,10.238 7.92,10.238Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_3g_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_3g_mobiledata_updated.xml
new file mode 100644
index 0000000..8e632e6
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_3g_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="12.21dp"
+    android:viewportWidth="14.51"
+    android:viewportHeight="10.42">
+    <path
+        android:pathData="M2.827,10.416C2.225,10.416 1.726,10.311 1.329,10.101C0.937,9.891 0.629,9.606 0.405,9.247C0.181,8.888 0.051,8.58 0.013,8.323C-0.019,8.066 0.023,7.849 0.139,7.672C0.256,7.49 0.415,7.371 0.615,7.315C0.816,7.254 1.003,7.259 1.175,7.329C1.353,7.394 1.481,7.495 1.56,7.63C1.644,7.765 1.733,7.922 1.826,8.099C1.92,8.272 2.041,8.416 2.19,8.533C2.34,8.65 2.533,8.708 2.771,8.708C3.089,8.708 3.343,8.615 3.534,8.428C3.726,8.237 3.821,7.959 3.821,7.595L3.821,6.888C3.821,6.529 3.726,6.256 3.534,6.069C3.348,5.882 3.077,5.789 2.722,5.789L2.456,5.789C2.251,5.789 2.071,5.714 1.917,5.565C1.768,5.416 1.693,5.236 1.693,5.026C1.693,4.816 1.768,4.636 1.917,4.487C2.067,4.338 2.244,4.263 2.449,4.263L2.666,4.263C2.984,4.263 3.222,4.177 3.38,4.004C3.544,3.831 3.625,3.558 3.625,3.185L3.625,2.618C3.625,2.315 3.544,2.084 3.38,1.925C3.217,1.766 2.998,1.687 2.722,1.687C2.531,1.687 2.37,1.729 2.239,1.813C2.109,1.892 1.999,2.004 1.91,2.149C1.822,2.294 1.738,2.427 1.658,2.548C1.579,2.665 1.453,2.756 1.28,2.821C1.108,2.882 0.926,2.882 0.734,2.821C0.543,2.756 0.391,2.632 0.279,2.45C0.172,2.268 0.142,2.049 0.188,1.792C0.24,1.531 0.384,1.248 0.622,0.945C0.86,0.642 1.159,0.408 1.518,0.245C1.882,0.082 2.326,0 2.848,0C3.67,0 4.323,0.215 4.808,0.644C5.294,1.069 5.536,1.636 5.536,2.345L5.536,2.8C5.536,3.332 5.417,3.768 5.179,4.109C4.941,4.445 4.598,4.69 4.15,4.844L4.15,4.9C4.678,5.017 5.086,5.259 5.375,5.628C5.669,5.992 5.816,6.484 5.816,7.105L5.816,7.679C5.816,8.514 5.55,9.179 5.018,9.674C4.491,10.169 3.761,10.416 2.827,10.416ZM10.943,10.416C9.819,10.411 8.92,10.031 8.248,9.275C7.581,8.514 7.247,7.406 7.247,5.95L7.247,4.417C7.247,2.975 7.588,1.878 8.269,1.127C8.951,0.371 9.863,-0.005 11.006,0C11.52,0 11.954,0.075 12.308,0.224C12.668,0.369 12.973,0.576 13.225,0.847C13.482,1.113 13.659,1.374 13.757,1.631C13.855,1.883 13.862,2.121 13.778,2.345C13.694,2.569 13.55,2.732 13.344,2.835C13.144,2.938 12.948,2.968 12.756,2.926C12.565,2.884 12.416,2.802 12.308,2.681C12.201,2.555 12.091,2.422 11.979,2.282C11.867,2.142 11.727,2.032 11.559,1.953C11.391,1.869 11.191,1.827 10.957,1.827C10.439,1.822 10.024,2.011 9.711,2.394C9.403,2.777 9.249,3.358 9.249,4.137L9.249,6.293C9.249,7.063 9.408,7.644 9.725,8.036C10.047,8.428 10.467,8.624 10.985,8.624C11.489,8.624 11.884,8.477 12.168,8.183C12.453,7.889 12.607,7.474 12.63,6.937L12.63,6.265L11.657,6.265C11.452,6.265 11.275,6.188 11.125,6.034C10.976,5.875 10.901,5.689 10.901,5.474C10.901,5.255 10.978,5.068 11.132,4.914C11.291,4.755 11.48,4.676 11.699,4.676L13.666,4.676C13.9,4.676 14.098,4.755 14.261,4.914C14.425,5.073 14.509,5.269 14.513,5.502L14.513,6.538C14.513,7.77 14.191,8.724 13.547,9.401C12.908,10.078 12.04,10.416 10.943,10.416Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_4g_lte_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_4g_lte_mobiledata_updated.xml
new file mode 100644
index 0000000..bba359e
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_4g_lte_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="34dp"
+    android:height="14.07dp"
+    android:viewportWidth="27.29"
+    android:viewportHeight="11.29">
+    <path
+        android:pathData="M4.552,11.195C4.286,11.195 4.058,11.099 3.866,10.908C3.68,10.717 3.586,10.488 3.586,10.222L3.586,8.171L3.74,7.891L3.74,2.823L4.72,3.53L3.712,3.53L1.808,7.366L4.37,7.366L4.797,7.275L5.777,7.275C6.015,7.275 6.218,7.359 6.386,7.527C6.554,7.695 6.638,7.898 6.638,8.136C6.638,8.369 6.554,8.57 6.386,8.738C6.218,8.906 6.015,8.99 5.777,8.99L1.192,8.99C0.856,8.99 0.572,8.88 0.338,8.661C0.11,8.442 -0.005,8.169 -0.005,7.842C-0.005,7.683 0.016,7.569 0.058,7.499C0.1,7.424 0.142,7.35 0.184,7.275L3.124,1.633C3.222,1.446 3.374,1.283 3.579,1.143C3.789,1.003 4.013,0.933 4.251,0.933C4.606,0.933 4.905,1.061 5.147,1.318C5.39,1.57 5.511,1.876 5.511,2.235L5.511,10.222C5.511,10.488 5.416,10.717 5.224,10.908C5.038,11.099 4.814,11.195 4.552,11.195ZM11.303,11.286C10.179,11.281 9.28,10.901 8.608,10.145C7.941,9.384 7.607,8.276 7.607,6.82L7.607,5.287C7.607,3.845 7.948,2.748 8.629,1.997C9.311,1.241 10.223,0.865 11.366,0.87C11.88,0.87 12.314,0.945 12.668,1.094C13.028,1.239 13.333,1.446 13.585,1.717C13.842,1.983 14.019,2.244 14.117,2.501C14.215,2.753 14.222,2.991 14.138,3.215C14.054,3.439 13.91,3.602 13.704,3.705C13.504,3.808 13.308,3.838 13.116,3.796C12.925,3.754 12.776,3.672 12.668,3.551C12.561,3.425 12.451,3.292 12.339,3.152C12.227,3.012 12.087,2.902 11.919,2.823C11.751,2.739 11.551,2.697 11.317,2.697C10.799,2.692 10.384,2.881 10.071,3.264C9.763,3.647 9.609,4.228 9.609,5.007L9.609,7.163C9.609,7.933 9.768,8.514 10.085,8.906C10.407,9.298 10.827,9.494 11.345,9.494C11.849,9.494 12.244,9.347 12.528,9.053C12.813,8.759 12.967,8.344 12.99,7.807L12.99,7.135L12.017,7.135C11.812,7.135 11.635,7.058 11.485,6.904C11.336,6.745 11.261,6.559 11.261,6.344C11.261,6.125 11.338,5.938 11.492,5.784C11.651,5.625 11.84,5.546 12.059,5.546L14.026,5.546C14.26,5.546 14.458,5.625 14.621,5.784C14.785,5.943 14.869,6.139 14.873,6.372L14.873,7.408C14.873,8.64 14.551,9.594 13.907,10.271C13.268,10.948 12.4,11.286 11.303,11.286ZM16.641,6.09C16.434,6.09 16.256,6.016 16.108,5.867C15.962,5.719 15.89,5.543 15.89,5.338L15.89,0.689C15.89,0.501 15.957,0.34 16.091,0.206C16.226,0.069 16.385,0 16.57,0C16.758,0 16.919,0.069 17.053,0.206C17.187,0.34 17.255,0.501 17.255,0.689L17.255,4.83L18.54,4.83C18.713,4.83 18.863,4.892 18.989,5.015C19.115,5.138 19.178,5.286 19.178,5.46C19.178,5.631 19.115,5.779 18.989,5.905C18.863,6.028 18.713,6.09 18.54,6.09L16.641,6.09ZM20.979,6.166C20.792,6.166 20.63,6.098 20.496,5.964C20.365,5.827 20.299,5.664 20.299,5.477L20.299,0.731L21.664,0.731L21.664,5.477C21.664,5.664 21.597,5.827 21.462,5.964C21.328,6.098 21.167,6.166 20.979,6.166ZM19.648,1.331C19.474,1.331 19.324,1.27 19.198,1.147C19.075,1.023 19.014,0.876 19.014,0.706C19.014,0.532 19.075,0.384 19.198,0.26C19.324,0.137 19.474,0.076 19.648,0.076L22.306,0.076C22.48,0.076 22.628,0.137 22.751,0.26C22.877,0.384 22.941,0.532 22.941,0.706C22.941,0.876 22.877,1.023 22.751,1.147C22.628,1.27 22.48,1.331 22.306,1.331L19.648,1.331ZM24.553,6.09C24.346,6.09 24.168,6.016 24.019,5.867C23.874,5.719 23.801,5.543 23.801,5.338L23.801,0.823C23.801,0.619 23.874,0.444 24.019,0.298C24.168,0.15 24.346,0.076 24.553,0.076L26.59,0.076C26.763,0.076 26.912,0.137 27.035,0.26C27.161,0.384 27.224,0.531 27.224,0.701C27.224,0.872 27.161,1.019 27.035,1.142C26.912,1.266 26.763,1.327 26.59,1.327L25.158,1.327L25.158,4.838L26.657,4.838C26.831,4.838 26.979,4.9 27.102,5.023C27.225,5.146 27.287,5.293 27.287,5.464C27.287,5.635 27.225,5.782 27.102,5.905C26.979,6.028 26.831,6.09 26.657,6.09L24.553,6.09ZM24.637,3.595L24.637,2.444L26.3,2.444C26.46,2.444 26.595,2.5 26.707,2.612C26.822,2.724 26.88,2.86 26.88,3.02C26.88,3.177 26.822,3.312 26.707,3.427C26.595,3.539 26.46,3.595 26.3,3.595L24.637,3.595Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_4g_lte_plus_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_4g_lte_plus_mobiledata_updated.xml
new file mode 100644
index 0000000..cb6fd50
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_4g_lte_plus_mobiledata_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="34dp"
+    android:height="11.93dp"
+    android:viewportWidth="32.18"
+    android:viewportHeight="11.29">
+    <path
+        android:pathData="M4.552,11.195C4.286,11.195 4.058,11.099 3.866,10.908C3.68,10.717 3.586,10.488 3.586,10.222L3.586,8.171L3.74,7.891L3.74,2.823L4.72,3.53L3.712,3.53L1.808,7.366L4.37,7.366L4.797,7.275L5.777,7.275C6.015,7.275 6.218,7.359 6.386,7.527C6.554,7.695 6.638,7.898 6.638,8.136C6.638,8.369 6.554,8.57 6.386,8.738C6.218,8.906 6.015,8.99 5.777,8.99L1.192,8.99C0.856,8.99 0.572,8.88 0.338,8.661C0.11,8.442 -0.005,8.169 -0.005,7.842C-0.005,7.683 0.016,7.569 0.058,7.499C0.1,7.424 0.142,7.35 0.184,7.275L3.124,1.633C3.222,1.446 3.374,1.283 3.579,1.143C3.789,1.003 4.013,0.933 4.251,0.933C4.606,0.933 4.905,1.061 5.147,1.318C5.39,1.57 5.511,1.876 5.511,2.235L5.511,10.222C5.511,10.488 5.416,10.717 5.224,10.908C5.038,11.099 4.814,11.195 4.552,11.195ZM11.303,11.286C10.179,11.281 9.28,10.901 8.608,10.145C7.941,9.384 7.607,8.276 7.607,6.82L7.607,5.287C7.607,3.845 7.948,2.748 8.629,1.997C9.311,1.241 10.223,0.865 11.366,0.87C11.88,0.87 12.314,0.945 12.668,1.094C13.028,1.239 13.333,1.446 13.585,1.717C13.842,1.983 14.019,2.244 14.117,2.501C14.215,2.753 14.222,2.991 14.138,3.215C14.054,3.439 13.91,3.602 13.704,3.705C13.504,3.808 13.308,3.838 13.116,3.796C12.925,3.754 12.776,3.672 12.668,3.551C12.561,3.425 12.451,3.292 12.339,3.152C12.227,3.012 12.087,2.902 11.919,2.823C11.751,2.739 11.551,2.697 11.317,2.697C10.799,2.692 10.384,2.881 10.071,3.264C9.763,3.647 9.609,4.228 9.609,5.007L9.609,7.163C9.609,7.933 9.768,8.514 10.085,8.906C10.407,9.298 10.827,9.494 11.345,9.494C11.849,9.494 12.244,9.347 12.528,9.053C12.813,8.759 12.967,8.344 12.99,7.807L12.99,7.135L12.017,7.135C11.812,7.135 11.635,7.058 11.485,6.904C11.336,6.745 11.261,6.559 11.261,6.344C11.261,6.125 11.338,5.938 11.492,5.784C11.651,5.625 11.84,5.546 12.059,5.546L14.026,5.546C14.26,5.546 14.458,5.625 14.621,5.784C14.785,5.943 14.869,6.139 14.873,6.372L14.873,7.408C14.873,8.64 14.551,9.594 13.907,10.271C13.268,10.948 12.4,11.286 11.303,11.286ZM16.641,6.09C16.434,6.09 16.256,6.016 16.108,5.867C15.962,5.719 15.89,5.543 15.89,5.338L15.89,0.689C15.89,0.501 15.957,0.34 16.091,0.206C16.226,0.069 16.385,0 16.57,0C16.758,0 16.919,0.069 17.053,0.206C17.187,0.34 17.255,0.501 17.255,0.689L17.255,4.83L18.54,4.83C18.713,4.83 18.863,4.892 18.989,5.015C19.115,5.138 19.178,5.286 19.178,5.46C19.178,5.631 19.115,5.779 18.989,5.905C18.863,6.028 18.713,6.09 18.54,6.09L16.641,6.09ZM20.979,6.166C20.792,6.166 20.63,6.098 20.496,5.964C20.365,5.827 20.299,5.664 20.299,5.477L20.299,0.731L21.664,0.731L21.664,5.477C21.664,5.664 21.597,5.827 21.462,5.964C21.328,6.098 21.167,6.166 20.979,6.166ZM19.648,1.331C19.474,1.331 19.324,1.27 19.198,1.147C19.075,1.023 19.014,0.876 19.014,0.706C19.014,0.532 19.075,0.384 19.198,0.26C19.324,0.137 19.474,0.076 19.648,0.076L22.306,0.076C22.48,0.076 22.628,0.137 22.751,0.26C22.877,0.384 22.941,0.532 22.941,0.706C22.941,0.876 22.877,1.023 22.751,1.147C22.628,1.27 22.48,1.331 22.306,1.331L19.648,1.331ZM24.553,6.09C24.346,6.09 24.168,6.016 24.019,5.867C23.874,5.719 23.801,5.543 23.801,5.338L23.801,0.823C23.801,0.619 23.874,0.444 24.019,0.298C24.168,0.15 24.346,0.076 24.553,0.076L26.59,0.076C26.763,0.076 26.912,0.137 27.035,0.26C27.161,0.384 27.224,0.531 27.224,0.701C27.224,0.872 27.161,1.019 27.035,1.142C26.912,1.266 26.763,1.327 26.59,1.327L25.158,1.327L25.158,4.838L26.657,4.838C26.831,4.838 26.979,4.9 27.102,5.023C27.225,5.146 27.287,5.293 27.287,5.464C27.287,5.635 27.225,5.782 27.102,5.905C26.979,6.028 26.831,6.09 26.657,6.09L24.553,6.09ZM24.637,3.595L24.637,2.444L26.3,2.444C26.46,2.444 26.595,2.5 26.707,2.612C26.822,2.724 26.88,2.86 26.88,3.02C26.88,3.177 26.822,3.312 26.707,3.427C26.595,3.539 26.46,3.595 26.3,3.595L24.637,3.595Z"
+        android:fillColor="#000"/>
+    <path
+        android:pathData="M30.101,5.346C29.923,5.346 29.768,5.282 29.637,5.154C29.509,5.023 29.445,4.867 29.445,4.686L29.445,1.742C29.445,1.561 29.509,1.406 29.637,1.278C29.768,1.147 29.923,1.082 30.101,1.082C30.28,1.082 30.433,1.147 30.561,1.278C30.689,1.406 30.753,1.561 30.753,1.742L30.753,4.686C30.753,4.867 30.689,5.023 30.561,5.154C30.433,5.282 30.28,5.346 30.101,5.346ZM28.677,3.858C28.499,3.858 28.345,3.795 28.217,3.67C28.089,3.542 28.025,3.39 28.025,3.214C28.025,3.038 28.089,2.887 28.217,2.762C28.345,2.634 28.499,2.57 28.677,2.57L31.525,2.57C31.704,2.57 31.857,2.634 31.985,2.762C32.113,2.887 32.177,3.038 32.177,3.214C32.177,3.39 32.113,3.542 31.985,3.67C31.857,3.795 31.704,3.858 31.525,3.858L28.677,3.858Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_4g_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_4g_mobiledata_updated.xml
new file mode 100644
index 0000000..562bcaf
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_4g_mobiledata_updated.xml
@@ -0,0 +1,25 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="19dp"
+    android:height="13.31dp"
+    android:viewportWidth="14.88"
+    android:viewportHeight="10.42">
+    <path
+        android:pathData="M4.552,10.325C4.286,10.325 4.058,10.229 3.866,10.038C3.68,9.847 3.586,9.618 3.586,9.352L3.586,7.301L3.74,7.021L3.74,1.953L4.72,2.66L3.712,2.66L1.808,6.496L4.37,6.496L4.797,6.405L5.777,6.405C6.015,6.405 6.218,6.489 6.386,6.657C6.554,6.825 6.638,7.028 6.638,7.266C6.638,7.499 6.554,7.7 6.386,7.868C6.218,8.036 6.015,8.12 5.777,8.12L1.192,8.12C0.856,8.12 0.572,8.01 0.338,7.791C0.11,7.572 -0.005,7.299 -0.005,6.972C-0.005,6.813 0.016,6.699 0.058,6.629C0.1,6.554 0.142,6.48 0.184,6.405L3.124,0.763C3.222,0.576 3.374,0.413 3.579,0.273C3.789,0.133 4.013,0.063 4.251,0.063C4.606,0.063 4.905,0.191 5.147,0.448C5.39,0.7 5.511,1.006 5.511,1.365L5.511,9.352C5.511,9.618 5.416,9.847 5.224,10.038C5.038,10.229 4.814,10.325 4.552,10.325ZM11.303,10.416C10.179,10.411 9.28,10.031 8.608,9.275C7.941,8.514 7.607,7.406 7.607,5.95L7.607,4.417C7.607,2.975 7.948,1.878 8.629,1.127C9.311,0.371 10.223,-0.005 11.366,0C11.88,0 12.314,0.075 12.668,0.224C13.028,0.369 13.333,0.576 13.585,0.847C13.842,1.113 14.019,1.374 14.117,1.631C14.215,1.883 14.222,2.121 14.138,2.345C14.054,2.569 13.91,2.732 13.704,2.835C13.504,2.938 13.308,2.968 13.116,2.926C12.925,2.884 12.776,2.802 12.668,2.681C12.561,2.555 12.451,2.422 12.339,2.282C12.227,2.142 12.087,2.032 11.919,1.953C11.751,1.869 11.551,1.827 11.317,1.827C10.799,1.822 10.384,2.011 10.071,2.394C9.763,2.777 9.609,3.358 9.609,4.137L9.609,6.293C9.609,7.063 9.768,7.644 10.085,8.036C10.407,8.428 10.827,8.624 11.345,8.624C11.849,8.624 12.244,8.477 12.528,8.183C12.813,7.889 12.967,7.474 12.99,6.937L12.99,6.265L12.017,6.265C11.812,6.265 11.635,6.188 11.485,6.034C11.336,5.875 11.261,5.689 11.261,5.474C11.261,5.255 11.338,5.068 11.492,4.914C11.651,4.755 11.84,4.676 12.059,4.676L14.026,4.676C14.26,4.676 14.458,4.755 14.621,4.914C14.785,5.073 14.869,5.269 14.873,5.502L14.873,6.538C14.873,7.77 14.551,8.724 13.907,9.401C13.268,10.078 12.4,10.416 11.303,10.416Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata_updated.xml
new file mode 100644
index 0000000..73e8994
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="23dp"
+    android:height="12.5dp"
+    android:viewportWidth="19.17"
+    android:viewportHeight="10.42">
+    <path
+        android:pathData="M4.552,10.325C4.286,10.325 4.058,10.229 3.866,10.038C3.68,9.847 3.586,9.618 3.586,9.352L3.586,7.301L3.74,7.021L3.74,1.953L4.72,2.66L3.712,2.66L1.808,6.496L4.37,6.496L4.797,6.405L5.777,6.405C6.015,6.405 6.218,6.489 6.386,6.657C6.554,6.825 6.638,7.028 6.638,7.266C6.638,7.499 6.554,7.7 6.386,7.868C6.218,8.036 6.015,8.12 5.777,8.12L1.192,8.12C0.856,8.12 0.572,8.01 0.338,7.791C0.11,7.572 -0.005,7.299 -0.005,6.972C-0.005,6.813 0.016,6.699 0.058,6.629C0.1,6.554 0.142,6.48 0.184,6.405L3.124,0.763C3.222,0.576 3.374,0.413 3.579,0.273C3.789,0.133 4.013,0.063 4.251,0.063C4.606,0.063 4.905,0.191 5.147,0.448C5.39,0.7 5.511,1.006 5.511,1.365L5.511,9.352C5.511,9.618 5.416,9.847 5.224,10.038C5.038,10.229 4.814,10.325 4.552,10.325ZM11.303,10.416C10.179,10.411 9.28,10.031 8.608,9.275C7.941,8.514 7.607,7.406 7.607,5.95L7.607,4.417C7.607,2.975 7.948,1.878 8.629,1.127C9.311,0.371 10.223,-0.005 11.366,0C11.88,0 12.314,0.075 12.668,0.224C13.028,0.369 13.333,0.576 13.585,0.847C13.842,1.113 14.019,1.374 14.117,1.631C14.215,1.883 14.222,2.121 14.138,2.345C14.054,2.569 13.91,2.732 13.704,2.835C13.504,2.938 13.308,2.968 13.116,2.926C12.925,2.884 12.776,2.802 12.668,2.681C12.561,2.555 12.451,2.422 12.339,2.282C12.227,2.142 12.087,2.032 11.919,1.953C11.751,1.869 11.551,1.827 11.317,1.827C10.799,1.822 10.384,2.011 10.071,2.394C9.763,2.777 9.609,3.358 9.609,4.137L9.609,6.293C9.609,7.063 9.768,7.644 10.085,8.036C10.407,8.428 10.827,8.624 11.345,8.624C11.849,8.624 12.244,8.477 12.528,8.183C12.813,7.889 12.967,7.474 12.99,6.937L12.99,6.265L12.017,6.265C11.812,6.265 11.635,6.188 11.485,6.034C11.336,5.875 11.261,5.689 11.261,5.474C11.261,5.255 11.338,5.068 11.492,4.914C11.651,4.755 11.84,4.676 12.059,4.676L14.026,4.676C14.26,4.676 14.458,4.755 14.621,4.914C14.785,5.073 14.869,5.269 14.873,5.502L14.873,6.538C14.873,7.77 14.551,8.724 13.907,9.401C13.268,10.078 12.4,10.416 11.303,10.416Z"
+        android:fillColor="#000"/>
+    <path
+        android:pathData="M17.086,4.476C16.907,4.476 16.752,4.412 16.622,4.284C16.494,4.153 16.43,3.997 16.43,3.816L16.43,0.872C16.43,0.691 16.494,0.536 16.622,0.408C16.752,0.277 16.907,0.212 17.086,0.212C17.264,0.212 17.418,0.277 17.546,0.408C17.674,0.536 17.738,0.691 17.738,0.872L17.738,3.816C17.738,3.997 17.674,4.153 17.546,4.284C17.418,4.412 17.264,4.476 17.086,4.476ZM15.662,2.988C15.483,2.988 15.33,2.925 15.202,2.8C15.074,2.672 15.01,2.52 15.01,2.344C15.01,2.168 15.074,2.017 15.202,1.892C15.33,1.764 15.483,1.7 15.662,1.7L18.51,1.7C18.688,1.7 18.842,1.764 18.97,1.892C19.098,2.017 19.162,2.168 19.162,2.344C19.162,2.52 19.098,2.672 18.97,2.8C18.842,2.925 18.688,2.988 18.51,2.988L15.662,2.988Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_5g_e_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_5g_e_mobiledata_updated.xml
new file mode 100644
index 0000000..c46da66
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_e_mobiledata_updated.xml
@@ -0,0 +1,25 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="22.0dp"
+    android:height="12.57dp"
+    android:viewportHeight="11.21"
+    android:viewportWidth="19.62">
+
+    <path
+        android:fillColor="#000"
+        android:pathData="M2.573,11.206C1.99,11.206 1.502,11.094 1.11,10.87C0.718,10.641 0.436,10.364 0.263,10.037C0.091,9.706 0.002,9.4 -0.003,9.12C-0.007,8.84 0.058,8.616 0.193,8.448C0.333,8.275 0.499,8.168 0.69,8.126C0.882,8.079 1.057,8.089 1.215,8.154C1.379,8.219 1.502,8.315 1.586,8.441C1.675,8.567 1.75,8.716 1.81,8.889C1.871,9.062 1.971,9.202 2.111,9.309C2.251,9.412 2.429,9.463 2.643,9.463C2.947,9.463 3.203,9.363 3.413,9.162C3.623,8.961 3.763,8.663 3.833,8.266L4.008,7.272C4.074,6.899 4.029,6.609 3.875,6.404C3.726,6.199 3.5,6.096 3.196,6.096C3,6.096 2.825,6.143 2.671,6.236C2.522,6.325 2.401,6.43 2.307,6.551C2.181,6.672 2.03,6.752 1.852,6.789C1.675,6.826 1.488,6.81 1.292,6.74C1.045,6.651 0.865,6.497 0.753,6.278C0.641,6.054 0.62,5.804 0.69,5.529L1.593,1.98C1.677,1.672 1.831,1.429 2.055,1.252C2.284,1.075 2.557,0.986 2.874,0.986L5.674,0.986C5.964,0.986 6.195,1.089 6.367,1.294C6.54,1.495 6.601,1.733 6.549,2.008C6.517,2.213 6.412,2.388 6.234,2.533C6.062,2.673 5.875,2.743 5.674,2.743L3.056,2.743L2.433,5.13L2.475,5.137C2.676,4.955 2.905,4.815 3.161,4.717C3.418,4.614 3.7,4.563 4.008,4.563C4.727,4.563 5.268,4.836 5.632,5.382C6.001,5.923 6.111,6.628 5.961,7.496L5.814,8.336C5.656,9.274 5.299,9.988 4.743,10.478C4.188,10.963 3.465,11.206 2.573,11.206ZM10.577,11.206C9.392,11.206 8.515,10.795 7.945,9.974C7.381,9.153 7.229,8.009 7.49,6.544L7.749,5.039C8.006,3.606 8.498,2.54 9.226,1.84C9.959,1.14 10.883,0.79 11.998,0.79C12.535,0.79 12.995,0.879 13.377,1.056C13.76,1.229 14.059,1.46 14.273,1.749C14.488,2.038 14.612,2.314 14.644,2.575C14.677,2.832 14.644,3.053 14.546,3.24C14.453,3.427 14.313,3.567 14.126,3.66C13.94,3.749 13.753,3.774 13.566,3.737C13.384,3.695 13.244,3.606 13.146,3.471C13.048,3.336 12.941,3.2 12.824,3.065C12.708,2.925 12.57,2.815 12.411,2.736C12.257,2.657 12.068,2.617 11.844,2.617C11.35,2.617 10.918,2.808 10.549,3.191C10.185,3.569 9.936,4.141 9.8,4.906L9.422,7.048C9.287,7.813 9.348,8.399 9.604,8.805C9.861,9.211 10.248,9.414 10.766,9.414C11.256,9.414 11.665,9.267 11.991,8.973C12.318,8.679 12.54,8.264 12.656,7.727L12.775,7.055L11.893,7.055C11.665,7.055 11.48,6.964 11.34,6.782C11.205,6.6 11.158,6.385 11.2,6.138C11.233,5.951 11.326,5.795 11.48,5.669C11.639,5.538 11.809,5.473 11.991,5.473L13.979,5.473C14.25,5.473 14.462,5.566 14.616,5.753C14.775,5.94 14.831,6.168 14.784,6.439L14.595,7.489C14.376,8.702 13.916,9.626 13.216,10.261C12.521,10.891 11.641,11.206 10.577,11.206ZM16.001,6.01C15.799,6.01 15.637,5.937 15.514,5.792C15.39,5.643 15.346,5.47 15.379,5.271L16.181,0.735C16.218,0.53 16.321,0.357 16.492,0.214C16.666,0.068 16.856,-0.004 17.063,-0.004L18.983,-0.004C19.187,-0.004 19.351,0.068 19.474,0.214C19.597,0.36 19.642,0.529 19.609,0.722C19.581,0.868 19.505,0.992 19.382,1.096C19.259,1.197 19.126,1.247 18.983,1.247L17.462,1.247L16.841,4.758L18.21,4.758C18.414,4.758 18.577,4.831 18.697,4.977C18.82,5.122 18.865,5.292 18.832,5.485C18.806,5.631 18.732,5.755 18.609,5.859C18.486,5.96 18.353,6.01 18.21,6.01L16.001,6.01ZM16.526,3.515L16.732,2.364L18.281,2.364C18.472,2.364 18.623,2.432 18.735,2.566C18.847,2.698 18.888,2.853 18.857,3.032C18.832,3.167 18.762,3.281 18.647,3.377C18.535,3.469 18.413,3.515 18.281,3.515L16.526,3.515Z" />
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_5g_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_5g_mobiledata_updated.xml
new file mode 100644
index 0000000..66787b0
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="12.28dp"
+    android:viewportWidth="14.42"
+    android:viewportHeight="10.42">
+    <path
+        android:pathData="M2.807,10.416C2.247,10.416 1.774,10.325 1.386,10.143C0.999,9.956 0.684,9.704 0.441,9.387C0.199,9.065 0.056,8.745 0.014,8.428C-0.028,8.111 0.014,7.859 0.14,7.672C0.271,7.485 0.439,7.366 0.644,7.315C0.85,7.259 1.036,7.266 1.204,7.336C1.372,7.406 1.494,7.506 1.568,7.637C1.643,7.763 1.727,7.912 1.82,8.085C1.914,8.253 2.03,8.393 2.17,8.505C2.315,8.617 2.504,8.673 2.737,8.673C3.055,8.673 3.309,8.57 3.5,8.365C3.696,8.16 3.794,7.849 3.794,7.434L3.794,6.461C3.794,6.083 3.701,5.796 3.514,5.6C3.332,5.399 3.092,5.299 2.793,5.299C2.593,5.299 2.427,5.341 2.296,5.425C2.166,5.509 2.054,5.605 1.96,5.712C1.858,5.819 1.715,5.901 1.533,5.957C1.351,6.008 1.148,6.001 0.924,5.936C0.682,5.861 0.49,5.714 0.35,5.495C0.215,5.271 0.159,5.035 0.182,4.788L0.455,1.281C0.483,0.987 0.614,0.733 0.847,0.518C1.081,0.303 1.347,0.196 1.645,0.196L4.515,0.196C4.758,0.196 4.966,0.282 5.138,0.455C5.316,0.628 5.404,0.833 5.404,1.071C5.404,1.314 5.316,1.521 5.138,1.694C4.966,1.867 4.758,1.953 4.515,1.953L2.044,1.953L1.841,4.34L1.897,4.354C2.07,4.172 2.285,4.03 2.541,3.927C2.803,3.824 3.094,3.773 3.416,3.773C4.126,3.773 4.697,4.02 5.131,4.515C5.565,5.005 5.782,5.677 5.782,6.531L5.782,7.378C5.782,8.33 5.516,9.074 4.984,9.611C4.452,10.148 3.727,10.416 2.807,10.416ZM10.853,10.416C9.729,10.411 8.83,10.031 8.158,9.275C7.491,8.514 7.157,7.406 7.157,5.95L7.157,4.417C7.157,2.975 7.498,1.878 8.179,1.127C8.861,0.371 9.773,-0.005 10.916,0C11.43,0 11.864,0.075 12.218,0.224C12.578,0.369 12.883,0.576 13.135,0.847C13.392,1.113 13.569,1.374 13.667,1.631C13.765,1.883 13.772,2.121 13.688,2.345C13.604,2.569 13.46,2.732 13.254,2.835C13.054,2.938 12.858,2.968 12.666,2.926C12.475,2.884 12.326,2.802 12.218,2.681C12.111,2.555 12.001,2.422 11.889,2.282C11.777,2.142 11.637,2.032 11.469,1.953C11.301,1.869 11.101,1.827 10.867,1.827C10.349,1.822 9.934,2.011 9.621,2.394C9.313,2.777 9.159,3.358 9.159,4.137L9.159,6.293C9.159,7.063 9.318,7.644 9.635,8.036C9.957,8.428 10.377,8.624 10.895,8.624C11.399,8.624 11.794,8.477 12.078,8.183C12.363,7.889 12.517,7.474 12.54,6.937L12.54,6.265L11.567,6.265C11.362,6.265 11.185,6.188 11.035,6.034C10.886,5.875 10.811,5.689 10.811,5.474C10.811,5.255 10.888,5.068 11.042,4.914C11.201,4.755 11.39,4.676 11.609,4.676L13.576,4.676C13.81,4.676 14.008,4.755 14.171,4.914C14.335,5.073 14.419,5.269 14.423,5.502L14.423,6.538C14.423,7.77 14.101,8.724 13.457,9.401C12.818,10.078 11.95,10.416 10.853,10.416Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default_updated.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default_updated.xml
new file mode 100644
index 0000000..5ba3c98
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="22dp"
+    android:height="11.63dp"
+    android:viewportWidth="19.71"
+    android:viewportHeight="10.42">
+  <path
+      android:pathData="M2.807,10.416C2.247,10.416 1.774,10.325 1.386,10.143C0.999,9.956 0.684,9.704 0.441,9.387C0.199,9.065 0.056,8.745 0.014,8.428C-0.028,8.111 0.014,7.859 0.14,7.672C0.271,7.485 0.439,7.366 0.644,7.315C0.85,7.259 1.036,7.266 1.204,7.336C1.372,7.406 1.494,7.506 1.568,7.637C1.643,7.763 1.727,7.912 1.82,8.085C1.914,8.253 2.03,8.393 2.17,8.505C2.315,8.617 2.504,8.673 2.737,8.673C3.055,8.673 3.309,8.57 3.5,8.365C3.696,8.16 3.794,7.849 3.794,7.434L3.794,6.461C3.794,6.083 3.701,5.796 3.514,5.6C3.332,5.399 3.092,5.299 2.793,5.299C2.593,5.299 2.427,5.341 2.296,5.425C2.166,5.509 2.054,5.605 1.96,5.712C1.858,5.819 1.715,5.901 1.533,5.957C1.351,6.008 1.148,6.001 0.924,5.936C0.682,5.861 0.49,5.714 0.35,5.495C0.215,5.271 0.159,5.035 0.182,4.788L0.455,1.281C0.483,0.987 0.614,0.733 0.847,0.518C1.081,0.303 1.347,0.196 1.645,0.196L4.515,0.196C4.758,0.196 4.966,0.282 5.138,0.455C5.316,0.628 5.404,0.833 5.404,1.071C5.404,1.314 5.316,1.521 5.138,1.694C4.966,1.867 4.758,1.953 4.515,1.953L2.044,1.953L1.841,4.34L1.897,4.354C2.07,4.172 2.285,4.03 2.541,3.927C2.803,3.824 3.094,3.773 3.416,3.773C4.126,3.773 4.697,4.02 5.131,4.515C5.565,5.005 5.782,5.677 5.782,6.531L5.782,7.378C5.782,8.33 5.516,9.074 4.984,9.611C4.452,10.148 3.727,10.416 2.807,10.416ZM10.853,10.416C9.729,10.411 8.83,10.031 8.158,9.275C7.491,8.514 7.157,7.406 7.157,5.95L7.157,4.417C7.157,2.975 7.498,1.878 8.179,1.127C8.861,0.371 9.773,-0.005 10.916,0C11.43,0 11.864,0.075 12.218,0.224C12.578,0.369 12.883,0.576 13.135,0.847C13.392,1.113 13.569,1.374 13.667,1.631C13.765,1.883 13.772,2.121 13.688,2.345C13.604,2.569 13.46,2.732 13.254,2.835C13.054,2.938 12.858,2.968 12.666,2.926C12.475,2.884 12.326,2.802 12.218,2.681C12.111,2.555 12.001,2.422 11.889,2.282C11.777,2.142 11.637,2.032 11.469,1.953C11.301,1.869 11.101,1.827 10.867,1.827C10.349,1.822 9.934,2.011 9.621,2.394C9.313,2.777 9.159,3.358 9.159,4.137L9.159,6.293C9.159,7.063 9.318,7.644 9.635,8.036C9.957,8.428 10.377,8.624 10.895,8.624C11.399,8.624 11.794,8.477 12.078,8.183C12.363,7.889 12.517,7.474 12.54,6.937L12.54,6.265L11.567,6.265C11.362,6.265 11.185,6.188 11.035,6.034C10.886,5.875 10.811,5.689 10.811,5.474C10.811,5.255 10.888,5.068 11.042,4.914C11.201,4.755 11.39,4.676 11.609,4.676L13.576,4.676C13.81,4.676 14.008,4.755 14.171,4.914C14.335,5.073 14.419,5.269 14.423,5.502L14.423,6.538C14.423,7.77 14.101,8.724 13.457,9.401C12.818,10.078 11.95,10.416 10.853,10.416Z"
+      android:fillColor="#000"/>
+  <path
+      android:pathData="M17.636,4.476C17.457,4.476 17.302,4.412 17.172,4.284C17.044,4.153 16.98,3.997 16.98,3.816L16.98,0.872C16.98,0.691 17.044,0.536 17.172,0.408C17.302,0.277 17.457,0.212 17.636,0.212C17.814,0.212 17.968,0.277 18.096,0.408C18.224,0.536 18.288,0.691 18.288,0.872L18.288,3.816C18.288,3.997 18.224,4.153 18.096,4.284C17.968,4.412 17.814,4.476 17.636,4.476ZM16.212,2.988C16.033,2.988 15.88,2.925 15.752,2.8C15.624,2.672 15.56,2.52 15.56,2.344C15.56,2.168 15.624,2.017 15.752,1.892C15.88,1.764 16.033,1.7 16.212,1.7L19.06,1.7C19.238,1.7 19.392,1.764 19.52,1.892C19.648,2.017 19.712,2.168 19.712,2.344C19.712,2.52 19.648,2.672 19.52,2.8C19.392,2.925 19.238,2.988 19.06,2.988L16.212,2.988Z"
+      android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_updated.xml
new file mode 100644
index 0000000..5ba3c98
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="22dp"
+    android:height="11.63dp"
+    android:viewportWidth="19.71"
+    android:viewportHeight="10.42">
+  <path
+      android:pathData="M2.807,10.416C2.247,10.416 1.774,10.325 1.386,10.143C0.999,9.956 0.684,9.704 0.441,9.387C0.199,9.065 0.056,8.745 0.014,8.428C-0.028,8.111 0.014,7.859 0.14,7.672C0.271,7.485 0.439,7.366 0.644,7.315C0.85,7.259 1.036,7.266 1.204,7.336C1.372,7.406 1.494,7.506 1.568,7.637C1.643,7.763 1.727,7.912 1.82,8.085C1.914,8.253 2.03,8.393 2.17,8.505C2.315,8.617 2.504,8.673 2.737,8.673C3.055,8.673 3.309,8.57 3.5,8.365C3.696,8.16 3.794,7.849 3.794,7.434L3.794,6.461C3.794,6.083 3.701,5.796 3.514,5.6C3.332,5.399 3.092,5.299 2.793,5.299C2.593,5.299 2.427,5.341 2.296,5.425C2.166,5.509 2.054,5.605 1.96,5.712C1.858,5.819 1.715,5.901 1.533,5.957C1.351,6.008 1.148,6.001 0.924,5.936C0.682,5.861 0.49,5.714 0.35,5.495C0.215,5.271 0.159,5.035 0.182,4.788L0.455,1.281C0.483,0.987 0.614,0.733 0.847,0.518C1.081,0.303 1.347,0.196 1.645,0.196L4.515,0.196C4.758,0.196 4.966,0.282 5.138,0.455C5.316,0.628 5.404,0.833 5.404,1.071C5.404,1.314 5.316,1.521 5.138,1.694C4.966,1.867 4.758,1.953 4.515,1.953L2.044,1.953L1.841,4.34L1.897,4.354C2.07,4.172 2.285,4.03 2.541,3.927C2.803,3.824 3.094,3.773 3.416,3.773C4.126,3.773 4.697,4.02 5.131,4.515C5.565,5.005 5.782,5.677 5.782,6.531L5.782,7.378C5.782,8.33 5.516,9.074 4.984,9.611C4.452,10.148 3.727,10.416 2.807,10.416ZM10.853,10.416C9.729,10.411 8.83,10.031 8.158,9.275C7.491,8.514 7.157,7.406 7.157,5.95L7.157,4.417C7.157,2.975 7.498,1.878 8.179,1.127C8.861,0.371 9.773,-0.005 10.916,0C11.43,0 11.864,0.075 12.218,0.224C12.578,0.369 12.883,0.576 13.135,0.847C13.392,1.113 13.569,1.374 13.667,1.631C13.765,1.883 13.772,2.121 13.688,2.345C13.604,2.569 13.46,2.732 13.254,2.835C13.054,2.938 12.858,2.968 12.666,2.926C12.475,2.884 12.326,2.802 12.218,2.681C12.111,2.555 12.001,2.422 11.889,2.282C11.777,2.142 11.637,2.032 11.469,1.953C11.301,1.869 11.101,1.827 10.867,1.827C10.349,1.822 9.934,2.011 9.621,2.394C9.313,2.777 9.159,3.358 9.159,4.137L9.159,6.293C9.159,7.063 9.318,7.644 9.635,8.036C9.957,8.428 10.377,8.624 10.895,8.624C11.399,8.624 11.794,8.477 12.078,8.183C12.363,7.889 12.517,7.474 12.54,6.937L12.54,6.265L11.567,6.265C11.362,6.265 11.185,6.188 11.035,6.034C10.886,5.875 10.811,5.689 10.811,5.474C10.811,5.255 10.888,5.068 11.042,4.914C11.201,4.755 11.39,4.676 11.609,4.676L13.576,4.676C13.81,4.676 14.008,4.755 14.171,4.914C14.335,5.073 14.419,5.269 14.423,5.502L14.423,6.538C14.423,7.77 14.101,8.724 13.457,9.401C12.818,10.078 11.95,10.416 10.853,10.416Z"
+      android:fillColor="#000"/>
+  <path
+      android:pathData="M17.636,4.476C17.457,4.476 17.302,4.412 17.172,4.284C17.044,4.153 16.98,3.997 16.98,3.816L16.98,0.872C16.98,0.691 17.044,0.536 17.172,0.408C17.302,0.277 17.457,0.212 17.636,0.212C17.814,0.212 17.968,0.277 18.096,0.408C18.224,0.536 18.288,0.691 18.288,0.872L18.288,3.816C18.288,3.997 18.224,4.153 18.096,4.284C17.968,4.412 17.814,4.476 17.636,4.476ZM16.212,2.988C16.033,2.988 15.88,2.925 15.752,2.8C15.624,2.672 15.56,2.52 15.56,2.344C15.56,2.168 15.624,2.017 15.752,1.892C15.88,1.764 16.033,1.7 16.212,1.7L19.06,1.7C19.238,1.7 19.392,1.764 19.52,1.892C19.648,2.017 19.712,2.168 19.712,2.344C19.712,2.52 19.648,2.672 19.52,2.8C19.392,2.925 19.238,2.988 19.06,2.988L16.212,2.988Z"
+      android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_carrier_wifi_updated.xml b/packages/SettingsLib/res/drawable/ic_carrier_wifi_updated.xml
new file mode 100644
index 0000000..14db826
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_carrier_wifi_updated.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="19.0dp"
+    android:height="12.38dp"
+    android:viewportHeight="10.26"
+    android:viewportWidth="15.75">
+
+    <path
+        android:fillColor="#000"
+        android:pathData="M2.864,10.259C2.598,10.259 2.358,10.168 2.143,9.986C1.929,9.804 1.793,9.589 1.737,9.342L0.036,1.208C-0.043,0.877 0.013,0.594 0.204,0.361C0.4,0.128 0.657,0.011 0.974,0.011C1.208,0.011 1.418,0.093 1.604,0.256C1.791,0.419 1.908,0.611 1.954,0.83L2.885,5.828L3.039,6.92L3.088,6.92L3.249,5.828L4.278,0.851C4.325,0.622 4.446,0.424 4.642,0.256C4.843,0.088 5.065,0.004 5.307,0.004C5.55,0.004 5.769,0.088 5.965,0.256C6.166,0.424 6.292,0.625 6.343,0.858L7.358,5.814L7.526,6.913L7.575,6.913L7.729,5.814L8.653,0.781C8.695,0.571 8.805,0.391 8.982,0.242C9.16,0.093 9.356,0.018 9.57,0.018C9.874,0.018 10.114,0.128 10.291,0.347C10.473,0.566 10.529,0.828 10.459,1.131L8.765,9.342C8.709,9.594 8.574,9.811 8.359,9.993C8.145,10.17 7.902,10.259 7.631,10.259C7.37,10.259 7.132,10.17 6.917,9.993C6.703,9.811 6.57,9.594 6.518,9.342L5.44,4.19L5.279,3.133L5.23,3.133L5.069,4.183L3.977,9.342C3.926,9.589 3.793,9.804 3.578,9.986C3.364,10.168 3.126,10.259 2.864,10.259Z" />
+
+    <path
+        android:fillColor="#000"
+        android:pathData="M13.676,4.396C13.497,4.396 13.342,4.332 13.212,4.204C13.084,4.073 13.02,3.917 13.02,3.736L13.02,0.792C13.02,0.611 13.084,0.456 13.212,0.328C13.342,0.197 13.497,0.132 13.676,0.132C13.854,0.132 14.008,0.197 14.136,0.328C14.264,0.456 14.328,0.611 14.328,0.792L14.328,3.736C14.328,3.917 14.264,4.073 14.136,4.204C14.008,4.332 13.854,4.396 13.676,4.396ZM12.252,2.908C12.073,2.908 11.92,2.845 11.792,2.72C11.664,2.592 11.6,2.44 11.6,2.264C11.6,2.088 11.664,1.937 11.792,1.812C11.92,1.684 12.073,1.62 12.252,1.62L15.1,1.62C15.278,1.62 15.432,1.684 15.56,1.812C15.688,1.937 15.752,2.088 15.752,2.264C15.752,2.44 15.688,2.592 15.56,2.72C15.432,2.845 15.278,2.908 15.1,2.908L12.252,2.908Z" />
+
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_e_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_e_mobiledata_updated.xml
new file mode 100644
index 0000000..febcd87
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_e_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="31.08dp"
+    android:viewportWidth="5.48"
+    android:viewportHeight="10.02">
+    <path
+        android:pathData="M1.081,10.02C0.787,10.02 0.533,9.913 0.318,9.698C0.104,9.483 -0.004,9.229 -0.004,8.935L-0.004,1.081C-0.004,0.787 0.104,0.533 0.318,0.318C0.533,0.103 0.787,-0.004 1.081,-0.004L4.455,-0.004C4.707,-0.004 4.922,0.087 5.099,0.269C5.281,0.446 5.372,0.659 5.372,0.906C5.372,1.153 5.281,1.368 5.099,1.55C4.922,1.727 4.707,1.816 4.455,1.816L1.963,1.816L1.963,8.2L4.56,8.2C4.812,8.2 5.027,8.291 5.204,8.473C5.386,8.65 5.477,8.863 5.477,9.11C5.477,9.357 5.386,9.572 5.204,9.754C5.027,9.931 4.812,10.02 4.56,10.02L1.081,10.02ZM1.186,5.736L1.186,4.042L3.951,4.042C4.185,4.042 4.385,4.126 4.553,4.294C4.721,4.457 4.805,4.656 4.805,4.889C4.805,5.118 4.721,5.316 4.553,5.484C4.385,5.652 4.185,5.736 3.951,5.736L1.186,5.736Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_g_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_g_mobiledata_updated.xml
new file mode 100644
index 0000000..d719f7a
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_g_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="24.37dp"
+    android:viewportWidth="7.27"
+    android:viewportHeight="10.42">
+    <path
+        android:pathData="M3.696,10.416C2.571,10.411 1.673,10.031 1.001,9.275C0.333,8.514 -0,7.406 -0,5.95L-0,4.417C-0,2.975 0.34,1.878 1.022,1.127C1.703,0.371 2.615,-0.005 3.759,0C4.272,0 4.706,0.075 5.061,0.224C5.42,0.369 5.726,0.576 5.978,0.847C6.235,1.113 6.412,1.374 6.51,1.631C6.608,1.883 6.615,2.121 6.531,2.345C6.447,2.569 6.302,2.732 6.097,2.835C5.896,2.938 5.7,2.968 5.509,2.926C5.317,2.884 5.168,2.802 5.061,2.681C4.953,2.555 4.844,2.422 4.732,2.282C4.62,2.142 4.48,2.032 4.312,1.953C4.144,1.869 3.943,1.827 3.71,1.827C3.192,1.822 2.776,2.011 2.464,2.394C2.156,2.777 2.002,3.358 2.002,4.137L2.002,6.293C2.002,7.063 2.16,7.644 2.478,8.036C2.8,8.428 3.22,8.624 3.738,8.624C4.242,8.624 4.636,8.477 4.921,8.183C5.205,7.889 5.359,7.474 5.383,6.937L5.383,6.265L4.41,6.265C4.204,6.265 4.027,6.188 3.878,6.034C3.728,5.875 3.654,5.689 3.654,5.474C3.654,5.255 3.731,5.068 3.885,4.914C4.043,4.755 4.232,4.676 4.452,4.676L6.419,4.676C6.652,4.676 6.85,4.755 7.014,4.914C7.177,5.073 7.261,5.269 7.266,5.502L7.266,6.538C7.266,7.77 6.944,8.724 6.3,9.401C5.661,10.078 4.792,10.416 3.696,10.416Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_h_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_h_mobiledata_updated.xml
new file mode 100644
index 0000000..e60ff8cd
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_h_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="26.68dp"
+    android:viewportWidth="6.53"
+    android:viewportHeight="10.25">
+    <path
+        android:pathData="M5.547,10.252C5.277,10.252 5.043,10.154 4.847,9.958C4.656,9.757 4.56,9.522 4.56,9.251L4.56,1.005C4.56,0.73 4.656,0.494 4.847,0.298C5.043,0.102 5.277,0.004 5.547,0.004C5.818,0.004 6.049,0.102 6.24,0.298C6.436,0.494 6.534,0.73 6.534,1.005L6.534,9.251C6.534,9.522 6.436,9.757 6.24,9.958C6.049,10.154 5.818,10.252 5.547,10.252ZM0.99,10.252C0.72,10.252 0.486,10.154 0.29,9.958C0.099,9.757 0.003,9.522 0.003,9.251L0.003,1.005C0.003,0.73 0.099,0.494 0.29,0.298C0.486,0.102 0.72,0.004 0.99,0.004C1.261,0.004 1.492,0.102 1.683,0.298C1.879,0.494 1.977,0.73 1.977,1.005L1.977,9.251C1.977,9.522 1.879,9.757 1.683,9.958C1.492,10.154 1.261,10.252 0.99,10.252ZM1.151,5.933L1.151,4.106L5.484,4.106L5.484,5.933L1.151,5.933Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_h_plus_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_h_plus_mobiledata_updated.xml
new file mode 100644
index 0000000..e02df56
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_h_plus_mobiledata_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="13.52dp"
+    android:viewportWidth="12.13"
+    android:viewportHeight="10.25">
+    <path
+        android:pathData="M5.541,10.252C5.271,10.252 5.037,10.154 4.841,9.958C4.65,9.757 4.554,9.522 4.554,9.251L4.554,1.005C4.554,0.73 4.65,0.494 4.841,0.298C5.037,0.102 5.271,0.004 5.541,0.004C5.812,0.004 6.043,0.102 6.234,0.298C6.43,0.494 6.528,0.73 6.528,1.005L6.528,9.251C6.528,9.522 6.43,9.757 6.234,9.958C6.043,10.154 5.812,10.252 5.541,10.252ZM0.984,10.252C0.714,10.252 0.48,10.154 0.284,9.958C0.093,9.757 -0.003,9.522 -0.003,9.251L-0.003,1.005C-0.003,0.73 0.093,0.494 0.284,0.298C0.48,0.102 0.714,0.004 0.984,0.004C1.255,0.004 1.486,0.102 1.677,0.298C1.873,0.494 1.971,0.73 1.971,1.005L1.971,9.251C1.971,9.522 1.873,9.757 1.677,9.958C1.486,10.154 1.255,10.252 0.984,10.252ZM1.145,5.933L1.145,4.106L5.478,4.106L5.478,5.933L1.145,5.933Z"
+        android:fillColor="#000"/>
+    <path
+        android:pathData="M10.056,4.396C9.877,4.396 9.722,4.332 9.592,4.204C9.464,4.073 9.4,3.917 9.4,3.736L9.4,0.792C9.4,0.611 9.464,0.456 9.592,0.328C9.722,0.197 9.877,0.132 10.056,0.132C10.234,0.132 10.388,0.197 10.516,0.328C10.644,0.456 10.708,0.611 10.708,0.792L10.708,3.736C10.708,3.917 10.644,4.073 10.516,4.204C10.388,4.332 10.234,4.396 10.056,4.396ZM8.632,2.908C8.453,2.908 8.3,2.845 8.172,2.72C8.044,2.592 7.98,2.44 7.98,2.264C7.98,2.088 8.044,1.937 8.172,1.812C8.3,1.684 8.453,1.62 8.632,1.62L11.48,1.62C11.658,1.62 11.812,1.684 11.94,1.812C12.068,1.937 12.132,2.088 12.132,2.264C12.132,2.44 12.068,2.592 11.94,2.72C11.812,2.845 11.658,2.908 11.48,2.908L8.632,2.908Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_lte_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_lte_mobiledata_updated.xml
new file mode 100644
index 0000000..9d64439
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_lte_mobiledata_updated.xml
@@ -0,0 +1,24 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="11.92dp"
+    android:viewportWidth="17.2"
+    android:viewportHeight="10.25">
+    <path
+        android:pathData="M1.082,10.14C0.788,10.14 0.534,10.033 0.319,9.818C0.105,9.603 -0.003,9.349 -0.003,9.055L-0.003,1.005C-0.003,0.73 0.093,0.494 0.284,0.298C0.48,0.102 0.714,0.004 0.984,0.004C1.255,0.004 1.486,0.102 1.677,0.298C1.873,0.494 1.971,0.73 1.971,1.005L1.971,8.313L4.19,8.313C4.442,8.313 4.659,8.404 4.841,8.586C5.023,8.763 5.114,8.976 5.114,9.223C5.114,9.475 5.023,9.692 4.841,9.874C4.659,10.051 4.442,10.14 4.19,10.14L1.082,10.14ZM7.59,10.252C7.32,10.252 7.086,10.154 6.89,9.958C6.694,9.757 6.596,9.522 6.596,9.251L6.596,1.068L8.577,1.068L8.577,9.251C8.577,9.522 8.479,9.757 8.283,9.958C8.087,10.154 7.856,10.252 7.59,10.252ZM5.413,1.936C5.161,1.936 4.944,1.847 4.762,1.67C4.585,1.488 4.496,1.273 4.496,1.026C4.496,0.779 4.585,0.566 4.762,0.389C4.944,0.207 5.161,0.116 5.413,0.116L9.753,0.116C10.005,0.116 10.222,0.207 10.404,0.389C10.586,0.566 10.677,0.779 10.677,1.026C10.677,1.273 10.586,1.488 10.404,1.67C10.222,1.847 10.005,1.936 9.753,1.936L5.413,1.936ZM12.799,10.14C12.505,10.14 12.251,10.033 12.036,9.818C11.821,9.603 11.714,9.349 11.714,9.055L11.714,1.201C11.714,0.907 11.821,0.653 12.036,0.438C12.251,0.223 12.505,0.116 12.799,0.116L16.173,0.116C16.425,0.116 16.64,0.207 16.817,0.389C16.999,0.566 17.09,0.779 17.09,1.026C17.09,1.273 16.999,1.488 16.817,1.67C16.64,1.847 16.425,1.936 16.173,1.936L13.681,1.936L13.681,8.32L16.278,8.32C16.53,8.32 16.745,8.411 16.922,8.593C17.104,8.77 17.195,8.983 17.195,9.23C17.195,9.477 17.104,9.692 16.922,9.874C16.745,10.051 16.53,10.14 16.278,10.14L12.799,10.14ZM12.904,5.856L12.904,4.162L15.669,4.162C15.902,4.162 16.103,4.246 16.271,4.414C16.439,4.577 16.523,4.776 16.523,5.009C16.523,5.238 16.439,5.436 16.271,5.604C16.103,5.772 15.902,5.856 15.669,5.856L12.904,5.856Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata_updated.xml b/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata_updated.xml
new file mode 100644
index 0000000..7075516
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata_updated.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2025 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="25dp"
+    android:height="11.85dp"
+    android:viewportWidth="21.63"
+    android:viewportHeight="10.25">
+    <path
+        android:pathData="M1.082,10.14C0.788,10.14 0.534,10.033 0.319,9.818C0.105,9.603 -0.003,9.349 -0.003,9.055L-0.003,1.005C-0.003,0.73 0.093,0.494 0.284,0.298C0.48,0.102 0.714,0.004 0.984,0.004C1.255,0.004 1.486,0.102 1.677,0.298C1.873,0.494 1.971,0.73 1.971,1.005L1.971,8.313L4.19,8.313C4.442,8.313 4.659,8.404 4.841,8.586C5.023,8.763 5.114,8.976 5.114,9.223C5.114,9.475 5.023,9.692 4.841,9.874C4.659,10.051 4.442,10.14 4.19,10.14L1.082,10.14ZM7.59,10.252C7.32,10.252 7.086,10.154 6.89,9.958C6.694,9.757 6.596,9.522 6.596,9.251L6.596,1.068L8.577,1.068L8.577,9.251C8.577,9.522 8.479,9.757 8.283,9.958C8.087,10.154 7.856,10.252 7.59,10.252ZM5.413,1.936C5.161,1.936 4.944,1.847 4.762,1.67C4.585,1.488 4.496,1.273 4.496,1.026C4.496,0.779 4.585,0.566 4.762,0.389C4.944,0.207 5.161,0.116 5.413,0.116L9.753,0.116C10.005,0.116 10.222,0.207 10.404,0.389C10.586,0.566 10.677,0.779 10.677,1.026C10.677,1.273 10.586,1.488 10.404,1.67C10.222,1.847 10.005,1.936 9.753,1.936L5.413,1.936ZM12.799,10.14C12.505,10.14 12.251,10.033 12.036,9.818C11.821,9.603 11.714,9.349 11.714,9.055L11.714,1.201C11.714,0.907 11.821,0.653 12.036,0.438C12.251,0.223 12.505,0.116 12.799,0.116L16.173,0.116C16.425,0.116 16.64,0.207 16.817,0.389C16.999,0.566 17.09,0.779 17.09,1.026C17.09,1.273 16.999,1.488 16.817,1.67C16.64,1.847 16.425,1.936 16.173,1.936L13.681,1.936L13.681,8.32L16.278,8.32C16.53,8.32 16.745,8.411 16.922,8.593C17.104,8.77 17.195,8.983 17.195,9.23C17.195,9.477 17.104,9.692 16.922,9.874C16.745,10.051 16.53,10.14 16.278,10.14L12.799,10.14ZM12.904,5.856L12.904,4.162L15.669,4.162C15.902,4.162 16.103,4.246 16.271,4.414C16.439,4.577 16.523,4.776 16.523,5.009C16.523,5.238 16.439,5.436 16.271,5.604C16.103,5.772 15.902,5.856 15.669,5.856L12.904,5.856Z"
+        android:fillColor="#000"/>
+    <path
+        android:pathData="M19.556,4.396C19.377,4.396 19.222,4.332 19.092,4.204C18.964,4.073 18.9,3.917 18.9,3.736L18.9,0.792C18.9,0.611 18.964,0.456 19.092,0.328C19.222,0.197 19.377,0.132 19.556,0.132C19.734,0.132 19.888,0.197 20.016,0.328C20.144,0.456 20.208,0.611 20.208,0.792L20.208,3.736C20.208,3.917 20.144,4.073 20.016,4.204C19.888,4.332 19.734,4.396 19.556,4.396ZM18.132,2.908C17.953,2.908 17.8,2.845 17.672,2.72C17.544,2.592 17.48,2.44 17.48,2.264C17.48,2.088 17.544,1.937 17.672,1.812C17.8,1.684 17.953,1.62 18.132,1.62L20.98,1.62C21.158,1.62 21.312,1.684 21.44,1.812C21.568,1.937 21.632,2.088 21.632,2.264C21.632,2.44 21.568,2.592 21.44,2.72C21.312,2.845 21.158,2.908 20.98,2.908L18.132,2.908Z"
+        android:fillColor="#000"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
index d9a417f..54c8788 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,24 +14,24 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="14dp"
-        android:height="14dp"
-        android:viewportWidth="14.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
-        android:fillAlpha="0.24"
+        android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
index facc285..6015be8 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
@@ -1,28 +1,51 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="15dp"
-        android:height="14dp"
-        android:viewportWidth="15.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
+    <group>
+        <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+        <clip-path
+            android:pathData="
+            M0,0
+            V13.5,0
+            H13.5,20
+            V0,20
+            H0,0
+            M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+        <path
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+    </group>
     <path
-        android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+        android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
index 2c05a93..7c85b9e 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
@@ -14,28 +14,28 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
-        android:fillAlpha="0.24"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
-        android:fillAlpha="0.24"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
index 328e45e..f75ec57 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
@@ -1,32 +1,54 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M14,0.5C14,0.224 14.224,0 14.5,0H15.5C15.776,0 16,0.224 16,0.5V3H14V0.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0.5,11C0.224,11 0,11.224 0,11.5V13.5C0,13.776 0.224,14 0.5,14H1.5C1.776,14 2,13.776 2,13.5V11.5C2,11.224 1.776,11 1.5,11H0.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M4,8C3.724,8 3.5,8.224 3.5,8.5V13.5C3.5,13.776 3.724,14 4,14H5C5.276,14 5.5,13.776 5.5,13.5V8.5C5.5,8.224 5.276,8 5,8H4Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
index b9054ba..df89aef 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
@@ -14,23 +14,23 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="14dp"
-        android:height="14dp"
-        android:viewportWidth="14.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+        android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
index 03a9349..fb73b6b 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
@@ -1,27 +1,35 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="15dp"
-        android:height="14dp"
-        android:viewportWidth="15.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
+    <group>
+        <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+        <clip-path
+            android:pathData="
+            M0,0
+            V13.5,0
+            H13.5,20
+            V0,20
+            H0,0
+            M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+        <path
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+    </group>
     <path
-        android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+        android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
index 774e917..5b7d8da 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
@@ -14,27 +14,27 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
-        android:fillAlpha="0.24"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
index 343ec1b..27e233d 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
@@ -1,31 +1,53 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
index b699203..e7ebf6f 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,22 +14,22 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="14dp"
-        android:height="14dp"
-        android:viewportWidth="14.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+        android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+        android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
index ba8649b..49ae9e4 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
@@ -1,26 +1,49 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="15dp"
-        android:height="14dp"
-        android:viewportWidth="15.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
+    <group>
+        <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+        <clip-path
+            android:pathData="
+            M0,0
+            V13.5,0
+            H13.5,20
+            V0,20
+            H0,0
+            M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+        <path
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+    </group>
     <path
-        android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+        android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
index 43fa734..19387bc 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,26 +14,26 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
-        android:fillAlpha="0.24"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
index 6309e17..322ede6 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
@@ -1,30 +1,52 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
index 6a218b3..b84b658 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,21 +14,21 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="14dp"
-        android:height="14dp"
-        android:viewportWidth="14.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+        android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+        android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
index 27433c7..7c4c1c6 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
@@ -1,25 +1,48 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="15dp"
-        android:height="14dp"
-        android:viewportWidth="15.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
+    <group>
+        <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+        <clip-path
+            android:pathData="
+            M0,0
+            V13.5,0
+            H13.5,20
+            V0,20
+            H0,0
+            M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+        <path
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+            android:fillAlpha="0.45"
+            android:fillColor="#000"/>
+    </group>
     <path
-        android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+        android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
index 158ae01..973032f 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,25 +14,25 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
-        android:fillAlpha="0.24"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
index e0517cf..25c9520 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
@@ -1,29 +1,51 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
index 1ebd396..fc807fa 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,20 +14,20 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="14dp"
-        android:height="14dp"
-        android:viewportWidth="14.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
+        android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+        android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+        android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
index 4473c29..d23680d 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
@@ -1,24 +1,47 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="15dp"
-        android:height="14dp"
-        android:viewportWidth="15.0"
-        android:viewportHeight="14.0">
+    android:width="17dp"
+    android:height="12dp"
+    android:viewportWidth="16.0"
+    android:viewportHeight="12.0">
+    <group>
+        <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+        <clip-path
+            android:pathData="
+            M0,0
+            V13.5,0
+            H13.5,20
+            V0,20
+            H0,0
+            M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+        <path
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+            android:fillColor="#000"/>
+        <path
+            android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+            android:fillColor="#000"/>
+    </group>
     <path
-        android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+        android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
index 1ed6ac8..b1336d7 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,24 +14,24 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
-        android:fillAlpha="0.24"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+        android:fillAlpha="0.45"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
index 703e3ac..bf62535 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
@@ -1,28 +1,50 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillAlpha="0.45"
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
-        android:fillAlpha="0.3"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
index 420ffb6..fa9bedc 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2023 The Android Open Source Project
+     Copyright (C) 2025 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.
@@ -14,23 +14,23 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="16dp"
-        android:height="14dp"
-        android:viewportWidth="16.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportWidth="20.5"
+    android:viewportHeight="12.0">
     <path
-        android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+        android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
+        android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+        android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
+        android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
         android:fillColor="#000"/>
     <path
-        android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+        android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
         android:fillColor="#000"/>
 </vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
index e63ca77..1728bc7 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
@@ -1,27 +1,49 @@
+<!--
+     Copyright (C) 2025 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.
+-->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="18dp"
-        android:height="14dp"
-        android:viewportWidth="18.0"
-        android:viewportHeight="14.0">
+    android:width="21dp"
+    android:height="12dp"
+    android:viewportHeight="12.0"
+    android:viewportWidth="20.5">
+    <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+    <group>
+        <clip-path android:pathData="
+            M0,0
+            H20.5
+            V12.0
+            H0
+            Z
+            M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+        <path
+            android:fillColor="#000"
+            android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+    </group>
     <path
-        android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
-        android:fillColor="#000"/>
-    <path
-        android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
-        android:fillColor="#000"/>
+        android:fillColor="#000"
+        android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
 </vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4b0400f..91ec836 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -889,6 +889,9 @@
     <!-- Preference category for monitoring debugging development settings. [CHAR LIMIT=25] -->
     <string name="debug_monitoring_category">Monitoring</string>
 
+    <!-- Preference category to alter window management settings, [CHAR LIMIT=50] -->
+    <string name="window_management_category">Window Management</string>
+
     <!-- UI debug setting: always enable strict mode? [CHAR LIMIT=25] -->
     <string name="strict_mode">Strict mode enabled</string>
     <!-- UI debug setting: show strict mode summary [CHAR LIMIT=50] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index a00484a..522a436 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -555,22 +555,17 @@
      * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile
      *
      * @param cachedDevice the CachedBluetoothDevice
-     * @param audioManager audio manager to get the current audio profile
+     * @param isOngoingCall get the current audio profile based on if in phone call
      * @return if the device is AvailableMediaBluetoothDevice
      */
     @WorkerThread
     public static boolean isAvailableMediaBluetoothDevice(
-            CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
-        int audioMode = audioManager.getMode();
+            CachedBluetoothDevice cachedDevice, boolean isOngoingCall) {
         int currentAudioProfile;
 
-        if (audioMode == AudioManager.MODE_RINGTONE
-                || audioMode == AudioManager.MODE_IN_CALL
-                || audioMode == AudioManager.MODE_IN_COMMUNICATION) {
-            // in phone call
+        if (isOngoingCall) {
             currentAudioProfile = BluetoothProfile.HEADSET;
         } else {
-            // without phone call
             currentAudioProfile = BluetoothProfile.A2DP;
         }
 
@@ -859,22 +854,17 @@
      * currentAudioProfile
      *
      * @param cachedDevice the CachedBluetoothDevice
-     * @param audioManager audio manager to get the current audio profile
+     * @param isOngoingCall get the current audio profile based on if in phone call
      * @return if the device is AvailableMediaBluetoothDevice
      */
     @WorkerThread
     public static boolean isConnectedBluetoothDevice(
-            CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
-        int audioMode = audioManager.getMode();
+            CachedBluetoothDevice cachedDevice, boolean isOngoingCall) {
         int currentAudioProfile;
 
-        if (audioMode == AudioManager.MODE_RINGTONE
-                || audioMode == AudioManager.MODE_IN_CALL
-                || audioMode == AudioManager.MODE_IN_COMMUNICATION) {
-            // in phone call
+        if (isOngoingCall) {
             currentAudioProfile = BluetoothProfile.HEADSET;
         } else {
-            // without phone call
             currentAudioProfile = BluetoothProfile.A2DP;
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 97a345e..bb96041 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1988,6 +1988,17 @@
     }
 
     /**
+     * @return {@code true} if {@code cachedBluetoothDevice} has member which is LeAudio device
+     */
+    public boolean hasConnectedLeAudioMemberDevice() {
+        LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
+        return leAudio != null && getMemberDevice().stream().anyMatch(
+                cachedDevice -> cachedDevice != null && cachedDevice.getDevice() != null
+                        && leAudio.getConnectionStatus(cachedDevice.getDevice())
+                        == BluetoothProfile.STATE_CONNECTED);
+    }
+
+    /**
      * @return {@code true} if {@code cachedBluetoothDevice} supports broadcast assistant profile
      */
     public boolean isConnectedLeAudioBroadcastAssistantDevice() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
index 58e9355..985599c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
@@ -57,7 +57,7 @@
      * Summaries for scales smaller than "default" in order of smallest to
      * largest.
      */
-    private static final int[] SUMMARIES_SMALLER = new int[] {
+    private static final int[] SUMMARIES_SMALLER = new int[]{
             R.string.screen_zoom_summary_small
     };
 
@@ -65,7 +65,7 @@
      * Summaries for scales larger than "default" in order of smallest to
      * largest.
      */
-    private static final int[] SUMMARIES_LARGER = new int[] {
+    private static final int[] SUMMARIES_LARGER = new int[]{
             R.string.screen_zoom_summary_large,
             R.string.screen_zoom_summary_very_large,
             R.string.screen_zoom_summary_extremely_large,
@@ -108,7 +108,8 @@
      * Creates an instance that stores the density values for the smallest display that satisfies
      * the predicate. It is enough to store the values for one display because the same density
      * should be set to all the displays that satisfy the predicate.
-     * @param context The context
+     *
+     * @param context   The context
      * @param predicate Determines what displays the density should be set for. The default display
      *                  must satisfy this predicate.
      */
@@ -127,14 +128,10 @@
             mCurrentIndex = -1;
             return;
         }
-        if (!mPredicate.test(defaultDisplayInfo)) {
-            throw new IllegalArgumentException(
-                    "Predicate must not filter out the default display.");
-        }
 
-        int idOfSmallestDisplay = Display.DEFAULT_DISPLAY;
-        int minDimensionPx = Math.min(defaultDisplayInfo.logicalWidth,
-                defaultDisplayInfo.logicalHeight);
+        int idOfSmallestDisplay = Display.INVALID_DISPLAY;
+        int minDimensionPx = Integer.MAX_VALUE;
+        DisplayInfo smallestDisplayInfo = null;
         for (Display display : mDisplayManager.getDisplays(
                 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
             DisplayInfo info = new DisplayInfo();
@@ -149,9 +146,19 @@
             if (minDimension < minDimensionPx) {
                 minDimensionPx = minDimension;
                 idOfSmallestDisplay = display.getDisplayId();
+                smallestDisplayInfo = info;
             }
         }
 
+        if (smallestDisplayInfo == null) {
+            Log.w(LOG_TAG, "No display satisfies the predicate");
+            mEntries = null;
+            mValues = null;
+            mDefaultDensity = 0;
+            mCurrentIndex = -1;
+            return;
+        }
+
         final int defaultDensity =
                 DisplayDensityUtils.getDefaultDensityForDisplay(idOfSmallestDisplay);
         if (defaultDensity <= 0) {
@@ -165,7 +172,12 @@
 
         final Resources res = context.getResources();
 
-        final int currentDensity = defaultDisplayInfo.logicalDensityDpi;
+        int currentDensity;
+        if (mPredicate.test(defaultDisplayInfo)) {
+            currentDensity = defaultDisplayInfo.logicalDensityDpi;
+        } else {
+            currentDensity = smallestDisplayInfo.logicalDensityDpi;
+        }
         int currentDensityIndex = -1;
 
         // Compute number of "larger" and "smaller" scales for this display.
@@ -266,16 +278,16 @@
      * Returns the default density for the specified display.
      *
      * @param displayId the identifier of the display
-     * @return the default density of the specified display, or {@code -1} if
-     *         the display does not exist or the density could not be obtained
+     * @return the default density of the specified display, or {@code -1} if the display does not
+     * exist or the density could not be obtained
      */
     private static int getDefaultDensityForDisplay(int displayId) {
-       try {
-           final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-           return wm.getInitialDisplayDensity(displayId);
-       } catch (RemoteException exc) {
-           return -1;
-       }
+        try {
+            final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+            return wm.getInitialDisplayDensity(displayId);
+        } catch (RemoteException exc) {
+            return -1;
+        }
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 51259e2f..e7887ed 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -295,9 +295,9 @@
     @WhenToDream
     public int getWhenToDreamSetting() {
         return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED
-                : isActivatedOnDock() ? WHILE_DOCKED
-                        : isActivatedOnPostured() ? WHILE_POSTURED
-                                : isActivatedOnSleep() ? WHILE_CHARGING
+                : isActivatedOnSleep() ? WHILE_CHARGING
+                        : isActivatedOnDock() ? WHILE_DOCKED
+                                : isActivatedOnPostured() ? WHILE_POSTURED
                                         : NEVER;
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index 13a0601..671dfa2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -128,12 +128,20 @@
 
     @Override
     public int getIntrinsicWidth() {
-        return mIntrinsicSize;
+        if (newStatusBarIcons()) {
+            return super.getIntrinsicWidth();
+        } else {
+            return mIntrinsicSize;
+        }
     }
 
     @Override
     public int getIntrinsicHeight() {
-        return mIntrinsicSize;
+        if (newStatusBarIcons()) {
+            return super.getIntrinsicHeight();
+        } else {
+            return mIntrinsicSize;
+        }
     }
 
     private void updateAnimation() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
index 094567c..9ca4623 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
@@ -18,6 +18,7 @@
 
 import com.android.settingslib.R;
 import com.android.settingslib.SignalIcon.MobileIconGroup;
+import com.android.settingslib.flags.Flags;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -29,22 +30,47 @@
     //***** Data connection icons
     public static final int FLIGHT_MODE_ICON = R.drawable.stat_sys_airplane_mode;
 
-    public static final int ICON_LTE = R.drawable.ic_lte_mobiledata;
-    public static final int ICON_LTE_PLUS = R.drawable.ic_lte_plus_mobiledata;
-    public static final int ICON_G = R.drawable.ic_g_mobiledata;
-    public static final int ICON_E = R.drawable.ic_e_mobiledata;
-    public static final int ICON_H = R.drawable.ic_h_mobiledata;
-    public static final int ICON_H_PLUS = R.drawable.ic_h_plus_mobiledata;
-    public static final int ICON_3G = R.drawable.ic_3g_mobiledata;
-    public static final int ICON_4G = R.drawable.ic_4g_mobiledata;
-    public static final int ICON_4G_PLUS = R.drawable.ic_4g_plus_mobiledata;
-    public static final int ICON_4G_LTE = R.drawable.ic_4g_lte_mobiledata;
-    public static final int ICON_4G_LTE_PLUS = R.drawable.ic_4g_lte_plus_mobiledata;
-    public static final int ICON_5G_E = R.drawable.ic_5g_e_mobiledata;
-    public static final int ICON_1X = R.drawable.ic_1x_mobiledata;
-    public static final int ICON_5G = R.drawable.ic_5g_mobiledata;
-    public static final int ICON_5G_PLUS = R.drawable.ic_5g_plus_mobiledata;
-    public static final int ICON_CWF = R.drawable.ic_carrier_wifi;
+    public static final int ICON_LTE =
+            flagged(R.drawable.ic_lte_mobiledata, R.drawable.ic_lte_mobiledata_updated);
+    public static final int ICON_LTE_PLUS =
+            flagged(R.drawable.ic_lte_plus_mobiledata, R.drawable.ic_lte_plus_mobiledata_updated);
+    public static final int ICON_G =
+            flagged(R.drawable.ic_g_mobiledata, R.drawable.ic_g_mobiledata_updated);
+    public static final int ICON_E =
+            flagged(R.drawable.ic_e_mobiledata, R.drawable.ic_e_mobiledata_updated);
+    public static final int ICON_H =
+            flagged(R.drawable.ic_h_mobiledata, R.drawable.ic_h_mobiledata_updated);
+    public static final int ICON_H_PLUS =
+            flagged(R.drawable.ic_h_plus_mobiledata, R.drawable.ic_h_plus_mobiledata_updated);
+    public static final int ICON_3G =
+            flagged(R.drawable.ic_3g_mobiledata, R.drawable.ic_3g_mobiledata_updated);
+    public static final int ICON_4G =
+            flagged(R.drawable.ic_4g_mobiledata, R.drawable.ic_4g_mobiledata_updated);
+    public static final int ICON_4G_PLUS =
+            flagged(R.drawable.ic_4g_plus_mobiledata, R.drawable.ic_4g_plus_mobiledata_updated);
+    public static final int ICON_4G_LTE =
+            flagged(R.drawable.ic_4g_lte_mobiledata, R.drawable.ic_4g_lte_mobiledata_updated);
+    public static final int ICON_4G_LTE_PLUS =
+            flagged(R.drawable.ic_4g_lte_plus_mobiledata,
+                    R.drawable.ic_4g_lte_plus_mobiledata_updated);
+    public static final int ICON_5G_E =
+            flagged(R.drawable.ic_5g_e_mobiledata, R.drawable.ic_5g_e_mobiledata_updated);
+    public static final int ICON_1X =
+            flagged(R.drawable.ic_1x_mobiledata, R.drawable.ic_1x_mobiledata_updated);
+    public static final int ICON_5G =
+            flagged(R.drawable.ic_5g_mobiledata, R.drawable.ic_5g_mobiledata_updated);
+    public static final int ICON_5G_PLUS =
+            flagged(R.drawable.ic_5g_plus_mobiledata, R.drawable.ic_5g_plus_mobiledata_updated);
+    public static final int ICON_CWF =
+            flagged(R.drawable.ic_carrier_wifi, R.drawable.ic_carrier_wifi_updated);
+
+    /** Make it slightly more obvious which resource we are using */
+    private static int flagged(int oldIcon, int newIcon) {
+        if (Flags.newStatusBarIcons()) {
+            return newIcon;
+        }
+        return oldIcon;
+    }
 
     public static final MobileIconGroup CARRIER_NETWORK_CHANGE = new MobileIconGroup(
             "CARRIER_NETWORK_CHANGE",
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index f446bb8..c4e7245 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -93,10 +93,9 @@
                     IntentFilter().apply {
                         addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
                         addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
-                        if (android.app.Flags.modesApi())
-                            addAction(
-                                NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
-                            )
+                        addAction(
+                            NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
+                        )
                     },
                     /* broadcastPermission = */ null,
                     /* scheduler = */ backgroundHandler,
@@ -109,16 +108,13 @@
     }
 
     override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
-        if (android.app.Flags.modesApi())
-            flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
-                // If available, get the value from extras to avoid a potential binder call.
-                it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY)
-                    ?: notificationManager.consolidatedNotificationPolicy
-            }
-        else
-            flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) {
-                notificationManager.consolidatedNotificationPolicy
-            }
+        flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
+            // If available, get the value from extras to avoid a potential binder call.
+            it?.extras?.getParcelable(
+                EXTRA_NOTIFICATION_POLICY,
+                NotificationManager.Policy::class.java
+            ) ?: notificationManager.consolidatedNotificationPolicy
+        }
     }
 
     override val globalZenMode: StateFlow<Int?> by lazy {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
index f0e7fb8..52d62b6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
@@ -19,7 +19,6 @@
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.AlertDialog;
-import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -42,8 +41,6 @@
 import android.widget.ScrollView;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
-
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.PhoneWindow;
 import com.android.settingslib.R;
@@ -80,7 +77,6 @@
     private static final int SECONDS_MS = 1000;
     private static final int MINUTES_MS = 60 * SECONDS_MS;
 
-    @Nullable
     private final EnableDndDialogMetricsLogger mMetricsLogger;
 
     @VisibleForTesting
@@ -152,16 +148,10 @@
                                     Slog.d(TAG, "Invalid manual condition: " + tag.condition);
                                 }
                                 // always triggers priority-only dnd with chosen condition
-                                if (Flags.modesApi()) {
-                                    mNotificationManager.setZenMode(
-                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                            getRealConditionId(tag.condition), TAG,
-                                            /* fromUser= */ true);
-                                } else {
-                                    mNotificationManager.setZenMode(
-                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                            getRealConditionId(tag.condition), TAG);
-                                }
+                                mNotificationManager.setZenMode(
+                                        Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                        getRealConditionId(tag.condition), TAG,
+                                        /* fromUser= */ true);
                             }
                         });
 
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/display/DisplayDensityUtilsTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/display/DisplayDensityUtilsTest.java
index bba278a..73cf28f 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/display/DisplayDensityUtilsTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/display/DisplayDensityUtilsTest.java
@@ -88,8 +88,8 @@
 
     @Test
     public void createDisplayDensityUtil_onlyDefaultDisplay() throws RemoteException {
-        var info = createDisplayInfoForDisplay(Display.DEFAULT_DISPLAY, Display.TYPE_INTERNAL, 2560,
-                1600, 320);
+        var info = createDisplayInfoForDisplay(
+                Display.DEFAULT_DISPLAY, Display.TYPE_INTERNAL, 2560, 1600, 320);
         var display = new Display(mDisplayManagerGlobal, info.displayId, info,
                 (DisplayAdjustments) null);
         doReturn(new Display[]{display}).when(mDisplayManager).getDisplays(any());
@@ -126,6 +126,33 @@
         assertThat(mDisplayDensityUtils.getValues()).isEqualTo(new int[]{330, 390, 426, 462, 500});
     }
 
+    @Test
+    public void createDisplayDensityUtil_forExternalDisplay() throws RemoteException {
+        // Default display
+        var defaultDisplayInfo = createDisplayInfoForDisplay(Display.DEFAULT_DISPLAY,
+                Display.TYPE_INTERNAL, 2000, 2000, 390);
+        var defaultDisplay = new Display(mDisplayManagerGlobal, defaultDisplayInfo.displayId,
+                defaultDisplayInfo,
+                (DisplayAdjustments) null);
+        doReturn(defaultDisplay).when(mDisplayManager).getDisplay(defaultDisplayInfo.displayId);
+
+        // Create external display
+        var externalDisplayInfo = createDisplayInfoForDisplay(/* displayId= */ 2,
+                Display.TYPE_EXTERNAL, 1920, 1080, 85);
+        var externalDisplay = new Display(mDisplayManagerGlobal, externalDisplayInfo.displayId,
+                externalDisplayInfo,
+                (DisplayAdjustments) null);
+
+        doReturn(new Display[]{externalDisplay, defaultDisplay}).when(mDisplayManager).getDisplays(
+                any());
+        doReturn(externalDisplay).when(mDisplayManager).getDisplay(externalDisplayInfo.displayId);
+
+        mDisplayDensityUtils = new DisplayDensityUtils(mContext,
+                (info) -> info.displayId == externalDisplayInfo.displayId);
+
+        assertThat(mDisplayDensityUtils.getValues()).isEqualTo(new int[]{72, 85, 94, 102, 112});
+    }
+
     private DisplayInfo createDisplayInfoForDisplay(int displayId, int displayType,
             int width, int height, int density) throws RemoteException {
         var displayInfo = new DisplayInfo();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 7c46db9..ebe6128 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -87,7 +87,6 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private BluetoothDevice mBluetoothDevice;
 
-    @Mock private AudioManager mAudioManager;
     @Mock private PackageManager mPackageManager;
     @Mock private LeAudioProfile mA2dpProfile;
     @Mock private LeAudioProfile mLeAudioProfile;
@@ -446,13 +445,12 @@
 
         assertThat(
                         BluetoothUtils.isAvailableMediaBluetoothDevice(
-                                mCachedBluetoothDevice, mAudioManager))
+                                mCachedBluetoothDevice, /* isOngoingCall= */ false))
                 .isTrue();
     }
 
     @Test
     public void isAvailableMediaBluetoothDevice_isHeadset_isConnectedA2dpDevice_returnFalse() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
@@ -460,13 +458,12 @@
 
         assertThat(
                         BluetoothUtils.isAvailableMediaBluetoothDevice(
-                                mCachedBluetoothDevice, mAudioManager))
+                                mCachedBluetoothDevice,  /* isOngoingCall= */ true))
                 .isFalse();
     }
 
     @Test
     public void isAvailableMediaBluetoothDevice_isA2dp_isConnectedA2dpDevice_returnTrue() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
@@ -474,13 +471,12 @@
 
         assertThat(
                         BluetoothUtils.isAvailableMediaBluetoothDevice(
-                                mCachedBluetoothDevice, mAudioManager))
+                                mCachedBluetoothDevice,  /* isOngoingCall= */ false))
                 .isTrue();
     }
 
     @Test
     public void isAvailableMediaBluetoothDevice_isHeadset_isConnectedHfpDevice_returnTrue() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
         when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
@@ -488,7 +484,7 @@
 
         assertThat(
                         BluetoothUtils.isAvailableMediaBluetoothDevice(
-                                mCachedBluetoothDevice, mAudioManager))
+                                mCachedBluetoothDevice, /* isOngoingCall= */ true))
                 .isTrue();
     }
 
@@ -499,56 +495,52 @@
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mBluetoothDevice.isConnected()).thenReturn(true);
 
-        assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice, mAudioManager))
-                .isFalse();
+        assertThat(BluetoothUtils.isConnectedBluetoothDevice(
+                mCachedBluetoothDevice, /* isOngoingCall= */ false)).isFalse();
     }
 
     @Test
     public void isConnectedBluetoothDevice_isHeadset_isConnectedA2dpDevice_returnTrue() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mBluetoothDevice.isConnected()).thenReturn(true);
 
-        assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice, mAudioManager))
-                .isTrue();
+        assertThat(BluetoothUtils.isConnectedBluetoothDevice(
+                mCachedBluetoothDevice,  /* isOngoingCall= */ true)).isTrue();
     }
 
     @Test
     public void isConnectedBluetoothDevice_isA2dp_isConnectedA2dpDevice_returnFalse() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mBluetoothDevice.isConnected()).thenReturn(true);
 
-        assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice, mAudioManager))
-                .isFalse();
+        assertThat(BluetoothUtils.isConnectedBluetoothDevice(
+                mCachedBluetoothDevice, /* isOngoingCall= */ false)).isFalse();
     }
 
     @Test
     public void isConnectedBluetoothDevice_isHeadset_isConnectedHfpDevice_returnFalse() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
         when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mBluetoothDevice.isConnected()).thenReturn(true);
 
-        assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice, mAudioManager))
-                .isFalse();
+        assertThat(BluetoothUtils.isConnectedBluetoothDevice(
+                mCachedBluetoothDevice,  /* isOngoingCall= */ true)).isFalse();
     }
 
     @Test
     public void isConnectedBluetoothDevice_isNotConnected_returnFalse() {
-        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
         when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mBluetoothDevice.isConnected()).thenReturn(false);
 
-        assertThat(BluetoothUtils.isConnectedBluetoothDevice(mCachedBluetoothDevice, mAudioManager))
-                .isFalse();
+        assertThat(BluetoothUtils.isConnectedBluetoothDevice(
+                mCachedBluetoothDevice,  /* isOngoingCall= */ true)).isFalse();
     }
 
     @Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
index 00ae96c..c21eb0c 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
@@ -16,6 +16,8 @@
 package com.android.settingslib.dream;
 
 
+import static android.service.dreams.Flags.FLAG_ALLOW_DREAM_WHEN_POSTURED;
+
 import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_DATE;
 import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_HOME_CONTROLS;
 import static com.android.settingslib.dream.DreamBackend.COMPLICATION_TYPE_TIME;
@@ -28,6 +30,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
+import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 
 import org.junit.After;
@@ -173,6 +176,58 @@
                 .containsExactlyElementsIn(enabledComplications);
     }
 
+    @Test
+    @EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED)
+    public void testChargingAndPosturedBothEnabled() {
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                1
+        );
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                1
+        );
+
+        assertThat(mBackend.getWhenToDreamSetting()).isEqualTo(DreamBackend.WHILE_CHARGING);
+    }
+
+    @Test
+    public void testChargingAndDockedBothEnabled() {
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                1
+        );
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+                1
+        );
+
+        assertThat(mBackend.getWhenToDreamSetting()).isEqualTo(
+                DreamBackend.WHILE_CHARGING_OR_DOCKED);
+    }
+
+    @Test
+    @EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED)
+    public void testPosturedAndDockedBothEnabled() {
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                1
+        );
+        Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+                1
+        );
+
+        assertThat(mBackend.getWhenToDreamSetting()).isEqualTo(
+                DreamBackend.WHILE_DOCKED);
+    }
+
     private void setControlsEnabledOnLockscreen(boolean enabled) {
         Settings.Secure.putInt(
                 mContext.getContentResolver(),
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index 388af61..b364368 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -91,7 +91,6 @@
             )
     }
 
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @Test
     fun consolidatedPolicyChanges_repositoryEmits_flagsOn() {
         testScope.runTest {
@@ -110,7 +109,6 @@
         }
     }
 
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @Test
     fun consolidatedPolicyChanges_repositoryEmitsFromExtras() {
         testScope.runTest {
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 236654d..f5c0233 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -8,7 +8,6 @@
 acul@google.com
 adamcohen@google.com
 aioana@google.com
-alexchau@google.com
 alexflo@google.com
 andonian@google.com
 amiko@google.com
@@ -91,10 +90,8 @@
 rgl@google.com
 roosa@google.com
 saff@google.com
-samcackett@google.com
 santie@google.com
 shanh@google.com
-silvajordan@google.com
 snoeberger@google.com
 spdonghao@google.com
 steell@google.com
@@ -106,7 +103,6 @@
 tracyzhou@google.com
 tsuji@google.com
 twickham@google.com
-uwaisashraf@google.com
 vadimt@google.com
 valiiftime@google.com
 vanjan@google.com
@@ -121,3 +117,11 @@
 yurilin@google.com
 yuzhechen@google.com
 zakcohen@google.com
+
+# Overview eng team
+alexchau@google.com
+samcackett@google.com
+silvajordan@google.com
+uwaisashraf@google.com
+vinayjoglekar@google.com
+willosborn@google.com
diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp
index 088ec13..f5bff85 100644
--- a/packages/SystemUI/aconfig/Android.bp
+++ b/packages/SystemUI/aconfig/Android.bp
@@ -28,6 +28,7 @@
         "//frameworks/libs/systemui/tracinglib:__subpackages__",
         "//frameworks/base/services/accessibility:__subpackages__",
         "//frameworks/base/services/tests:__subpackages__",
+        "//packages/apps/Settings:__subpackages__",
         "//platform_testing:__subpackages__",
         "//vendor:__subpackages__",
         "//cts:__subpackages__",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0ccb20c..29b578a 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -974,6 +974,26 @@
 }
 
 flag {
+    name: "use_notif_inflation_thread_for_footer"
+    namespace: "systemui"
+    description: "use the @NotifInflation thread for FooterView and EmptyShadeView inflation"
+    bug: "375320642"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "use_notif_inflation_thread_for_row"
+    namespace: "systemui"
+    description: "use the @NotifInflation thread for ExpandableNotificationRow inflation"
+    bug: "375320642"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "notify_power_manager_user_activity_background"
     namespace: "systemui"
     description: "Decide whether to notify the user activity to power manager in the background thread."
@@ -1977,6 +1997,16 @@
 }
 
 flag {
+    name: "hardware_color_styles"
+    namespace: "systemui"
+    description: "Enables loading initial colors based ion hardware color"
+    bug: "347286986"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
    name: "shade_launch_accessibility"
    namespace: "systemui"
    description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events."
@@ -2026,3 +2056,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "skip_hide_sensitive_notif_animation"
+    namespace: "systemui"
+    description: "Skip hide sensitive notification animation when the showing layout is not changed."
+    bug: "390624334"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index 65cd3c7..444389f 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -36,6 +36,7 @@
 import android.widget.FrameLayout
 import com.android.internal.jank.Cuj.CujType
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
 import java.util.LinkedList
 import kotlin.math.min
 import kotlin.math.roundToInt
@@ -58,7 +59,7 @@
 @JvmOverloads
 constructor(
     /** The view that will be ghosted and from which the background will be extracted. */
-    private val ghostedView: View,
+    transitioningView: View,
 
     /** The [CujType] associated to this launch animation. */
     private val launchCujType: Int? = null,
@@ -75,11 +76,24 @@
     private val isEphemeral: Boolean = false,
     private var interactionJankMonitor: InteractionJankMonitor =
         InteractionJankMonitor.getInstance(),
+
+    /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */
+    private val transitionRegistry: IViewTransitionRegistry? =
+        if (Flags.decoupleViewControllerInAnimlib()) {
+            ViewTransitionRegistry.instance
+        } else {
+            null
+        }
 ) : ActivityTransitionAnimator.Controller {
     override val isLaunching: Boolean = true
 
     /** The container to which we will add the ghost view and expanding background. */
-    override var transitionContainer = ghostedView.rootView as ViewGroup
+    override var transitionContainer: ViewGroup
+        get() = ghostedView.rootView as ViewGroup
+        set(_) {
+            // empty, should never be set to avoid memory leak
+        }
+
     private val transitionContainerOverlay: ViewGroupOverlay
         get() = transitionContainer.overlay
 
@@ -138,9 +152,33 @@
             }
         }
 
+    /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */
+    private val transitionToken =
+        if (Flags.decoupleViewControllerInAnimlib()) {
+            ViewTransitionToken(transitioningView::class.java)
+        } else {
+            null
+        }
+
+    /** The view that will be ghosted and from which the background will be extracted */
+    private val ghostedView: View
+        get() =
+            if (Flags.decoupleViewControllerInAnimlib()) {
+                transitionRegistry?.getView(transitionToken!!)
+            } else {
+                _ghostedView
+            }!!
+
+    private val _ghostedView =
+        if (Flags.decoupleViewControllerInAnimlib()) {
+            null
+        } else {
+            transitioningView
+        }
+
     init {
         // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
-        if (ghostedView !is LaunchableView) {
+        if (transitioningView !is LaunchableView) {
             throw IllegalArgumentException(
                 "A GhostedViewLaunchAnimatorController was created from a View that does not " +
                     "implement LaunchableView. This can lead to subtle bugs where the visibility " +
@@ -148,6 +186,10 @@
             )
         }
 
+        if (Flags.decoupleViewControllerInAnimlib()) {
+            transitionRegistry?.register(transitionToken!!, transitioningView)
+        }
+
         /** Find the first view with a background in [view] and its children. */
         fun findBackground(view: View): Drawable? {
             if (view.background != null) {
@@ -184,6 +226,7 @@
         if (TransitionAnimator.returnAnimationsEnabled()) {
             ghostedView.removeOnAttachStateChangeListener(detachListener)
         }
+        transitionToken?.let { token -> transitionRegistry?.unregister(token) }
     }
 
     /**
@@ -237,7 +280,7 @@
         val insets = backgroundInsets
         val boundCorrections: Rect =
             if (ghostedView is LaunchableView) {
-                ghostedView.getPaddingForLaunchAnimation()
+                (ghostedView as LaunchableView).getPaddingForLaunchAnimation()
             } else {
                 Rect()
             }
@@ -387,8 +430,8 @@
 
         if (ghostedView is LaunchableView) {
             // Restore the ghosted view visibility.
-            ghostedView.setShouldBlockVisibilityChanges(false)
-            ghostedView.onActivityLaunchAnimationEnd()
+            (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false)
+            (ghostedView as LaunchableView).onActivityLaunchAnimationEnd()
         } else {
             // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
             // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
@@ -398,7 +441,7 @@
             ghostedView.invalidate()
         }
 
-        if (isEphemeral) {
+        if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) {
             onDispose()
         }
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt
new file mode 100644
index 0000000..af3ca87
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 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.animation
+
+import android.view.View
+
+/** Represents a Registry for holding a transitioning view mapped to a token */
+interface IViewTransitionRegistry {
+
+    /**
+     * Registers the transitioning [view] mapped to a [token]
+     *
+     * @param token The token corresponding to the transitioning view
+     * @param view The view undergoing transition
+     */
+    fun register(token: ViewTransitionToken, view: View)
+
+    /**
+     * Unregisters the transitioned view from its corresponding [token]
+     *
+     * @param token The token corresponding to the transitioning view
+     */
+    fun unregister(token: ViewTransitionToken)
+
+    /**
+     * Extracts a transitioning view from registry using its corresponding [token]
+     *
+     * @param token The token corresponding to the transitioning view
+     */
+    fun getView(token: ViewTransitionToken): View?
+
+    /**
+     * Return token mapped to the [view], if it is present in the registry
+     *
+     * @param view the transitioning view whose token we are requesting
+     * @return token associated with the [view] if present, else null
+     */
+    fun getViewToken(view: View): ViewTransitionToken?
+
+    /** Event call to run on registry update (on both [register] and [unregister]) */
+    fun onRegistryUpdate()
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index b9f9bc7..5b073e4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -27,9 +27,8 @@
 import android.text.Layout
 import android.util.Log
 import android.util.LruCache
-
-private const val DEFAULT_ANIMATION_DURATION: Long = 300
-private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
+import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
 
 typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
 
@@ -76,6 +75,10 @@
             cache.put(fvar, it)
         }
     }
+
+    companion object {
+        private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
+    }
 }
 
 /**
@@ -108,25 +111,12 @@
     private val typefaceCache: TypefaceVariantCache,
     private val invalidateCallback: () -> Unit = {},
 ) {
-    // Following two members are for mutable for testing purposes.
-    public var textInterpolator = TextInterpolator(layout, typefaceCache)
-    public var animator =
-        ValueAnimator.ofFloat(1f).apply {
-            duration = DEFAULT_ANIMATION_DURATION
-            addUpdateListener {
-                textInterpolator.progress = it.animatedValue as Float
-                textInterpolator.linearProgress =
-                    it.currentPlayTime.toFloat() / it.duration.toFloat()
-                invalidateCallback()
-            }
-            addListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase()
+    @VisibleForTesting var textInterpolator = TextInterpolator(layout, typefaceCache)
+    @VisibleForTesting var createAnimator: () -> ValueAnimator = { ValueAnimator.ofFloat(1f) }
 
-                    override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase()
-                }
-            )
-        }
+    var animator: ValueAnimator? = null
+
+    val fontVariationUtils = FontVariationUtils()
 
     sealed class PositionedGlyph {
         /** Mutable X coordinate of the glyph position relative from drawing offset. */
@@ -165,8 +155,6 @@
             protected set
     }
 
-    private val fontVariationUtils = FontVariationUtils()
-
     fun updateLayout(layout: Layout, textSize: Float = -1f) {
         textInterpolator.layout = layout
 
@@ -178,9 +166,8 @@
         }
     }
 
-    fun isRunning(): Boolean {
-        return animator.isRunning
-    }
+    val isRunning: Boolean
+        get() = animator?.isRunning ?: false
 
     /**
      * GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
@@ -237,110 +224,110 @@
 
     fun draw(c: Canvas) = textInterpolator.draw(c)
 
-    /**
-     * Set text style with animation.
-     *
-     * ```
-     * By passing -1 to weight, the view preserve the current weight.
-     * By passing -1 to textSize, the view preserve the current text size.
-     * By passing -1 to duration, the default text animation, 1000ms, is used.
-     * By passing false to animate, the text will be updated without animation.
-     * ```
-     *
-     * @param fvar an optional text fontVariationSettings.
-     * @param textSize an optional font size.
-     * @param colors an optional colors array that must be the same size as numLines passed to the
-     *   TextInterpolator
-     * @param strokeWidth an optional paint stroke width
-     * @param animate an optional boolean indicating true for showing style transition as animation,
-     *   false for immediate style transition. True by default.
-     * @param duration an optional animation duration in milliseconds. This is ignored if animate is
-     *   false.
-     * @param interpolator an optional time interpolator. If null is passed, last set interpolator
-     *   will be used. This is ignored if animate is false.
-     */
-    fun setTextStyle(
-        fvar: String? = "",
-        textSize: Float = -1f,
-        color: Int? = null,
-        strokeWidth: Float = -1f,
-        animate: Boolean = true,
-        duration: Long = -1L,
-        interpolator: TimeInterpolator? = null,
-        delay: Long = 0,
-        onAnimationEnd: Runnable? = null,
+    /** Style spec to use when rendering the font */
+    data class Style(
+        val fVar: String? = null,
+        val textSize: Float? = null,
+        val color: Int? = null,
+        val strokeWidth: Float? = null,
     ) {
-        setTextStyleInternal(
-            fvar,
-            textSize,
-            color,
-            strokeWidth,
-            animate,
-            duration,
-            interpolator,
-            delay,
-            onAnimationEnd,
-            updateLayoutOnFailure = true,
-        )
+        fun withUpdatedFVar(
+            fontVariationUtils: FontVariationUtils,
+            weight: Int = -1,
+            width: Int = -1,
+            opticalSize: Int = -1,
+            roundness: Int = -1,
+        ): Style {
+            return this.copy(
+                fVar =
+                    fontVariationUtils.updateFontVariation(
+                        weight = weight,
+                        width = width,
+                        opticalSize = opticalSize,
+                        roundness = roundness,
+                    )
+            )
+        }
+    }
+
+    /** Animation Spec for use when style changes should be animated */
+    data class Animation(
+        val animate: Boolean = true,
+        val startDelay: Long = 0,
+        val duration: Long = DEFAULT_ANIMATION_DURATION,
+        val interpolator: TimeInterpolator = Interpolators.LINEAR,
+        val onAnimationEnd: Runnable? = null,
+    ) {
+        fun configureAnimator(animator: Animator) {
+            animator.startDelay = startDelay
+            animator.duration = duration
+            animator.interpolator = interpolator
+            if (onAnimationEnd != null) {
+                animator.addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            onAnimationEnd.run()
+                        }
+                    }
+                )
+            }
+        }
+
+        companion object {
+            val DISABLED = Animation(animate = false)
+        }
+    }
+
+    /** Sets the text style, optionally with animation */
+    fun setTextStyle(style: Style, animation: Animation = Animation.DISABLED) {
+        animator?.cancel()
+        setTextStyleInternal(style, rebase = animation.animate)
+
+        if (animation.animate) {
+            animator = buildAnimator(animation).apply { start() }
+        } else {
+            textInterpolator.progress = 1f
+            textInterpolator.rebase()
+            invalidateCallback()
+        }
+    }
+
+    /** Builds a ValueAnimator from the specified animation parameters */
+    private fun buildAnimator(animation: Animation): ValueAnimator {
+        return createAnimator().apply {
+            duration = DEFAULT_ANIMATION_DURATION
+            animation.configureAnimator(this)
+
+            addUpdateListener {
+                textInterpolator.progress = it.animatedValue as Float
+                textInterpolator.linearProgress = it.currentPlayTime / it.duration.toFloat()
+                invalidateCallback()
+            }
+
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animator: Animator) = textInterpolator.rebase()
+
+                    override fun onAnimationCancel(animator: Animator) = textInterpolator.rebase()
+                }
+            )
+        }
     }
 
     private fun setTextStyleInternal(
-        fvar: String?,
-        textSize: Float,
-        color: Int?,
-        strokeWidth: Float,
-        animate: Boolean,
-        duration: Long,
-        interpolator: TimeInterpolator?,
-        delay: Long,
-        onAnimationEnd: Runnable?,
-        updateLayoutOnFailure: Boolean,
+        style: Style,
+        rebase: Boolean,
+        updateLayoutOnFailure: Boolean = true,
     ) {
         try {
-            if (animate) {
-                animator.cancel()
-                textInterpolator.rebase()
-            }
-
-            if (textSize >= 0) {
-                textInterpolator.targetPaint.textSize = textSize
-            }
-            if (!fvar.isNullOrBlank()) {
-                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
-            }
-            if (color != null) {
-                textInterpolator.targetPaint.color = color
-            }
-            if (strokeWidth >= 0F) {
-                textInterpolator.targetPaint.strokeWidth = strokeWidth
+            if (rebase) textInterpolator.rebase()
+            style.color?.let { textInterpolator.targetPaint.color = it }
+            style.textSize?.let { textInterpolator.targetPaint.textSize = it }
+            style.strokeWidth?.let { textInterpolator.targetPaint.strokeWidth = it }
+            style.fVar?.let {
+                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(it)
             }
             textInterpolator.onTargetPaintModified()
-
-            if (animate) {
-                animator.startDelay = delay
-                animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration
-                interpolator?.let { animator.interpolator = it }
-                if (onAnimationEnd != null) {
-                    animator.addListener(
-                        object : AnimatorListenerAdapter() {
-                            override fun onAnimationEnd(animation: Animator) {
-                                onAnimationEnd.run()
-                                animator.removeListener(this)
-                            }
-
-                            override fun onAnimationCancel(animation: Animator) {
-                                animator.removeListener(this)
-                            }
-                        }
-                    )
-                }
-                animator.start()
-            } else {
-                // No animation is requested, thus set base and target state to the same state.
-                textInterpolator.progress = 1f
-                textInterpolator.rebase()
-                invalidateCallback()
-            }
         } catch (ex: IllegalArgumentException) {
             if (updateLayoutOnFailure) {
                 Log.e(
@@ -351,81 +338,15 @@
                 )
 
                 updateLayout(textInterpolator.layout)
-                setTextStyleInternal(
-                    fvar,
-                    textSize,
-                    color,
-                    strokeWidth,
-                    animate,
-                    duration,
-                    interpolator,
-                    delay,
-                    onAnimationEnd,
-                    updateLayoutOnFailure = false,
-                )
+                setTextStyleInternal(style, rebase, updateLayoutOnFailure = false)
             } else {
                 throw ex
             }
         }
     }
 
-    /**
-     * Set text style with animation. Similar as
-     *
-     * ```
-     * fun setTextStyle(
-     *      fvar: String? = "",
-     *      textSize: Float = -1f,
-     *      color: Int? = null,
-     *      strokeWidth: Float = -1f,
-     *      animate: Boolean = true,
-     *      duration: Long = -1L,
-     *      interpolator: TimeInterpolator? = null,
-     *      delay: Long = 0,
-     *      onAnimationEnd: Runnable? = null
-     * )
-     * ```
-     *
-     * @param weight an optional style value for `wght` in fontVariationSettings.
-     * @param width an optional style value for `wdth` in fontVariationSettings.
-     * @param opticalSize an optional style value for `opsz` in fontVariationSettings.
-     * @param roundness an optional style value for `ROND` in fontVariationSettings.
-     */
-    fun setTextStyle(
-        weight: Int = -1,
-        width: Int = -1,
-        opticalSize: Int = -1,
-        roundness: Int = -1,
-        textSize: Float = -1f,
-        color: Int? = null,
-        strokeWidth: Float = -1f,
-        animate: Boolean = true,
-        duration: Long = -1L,
-        interpolator: TimeInterpolator? = null,
-        delay: Long = 0,
-        onAnimationEnd: Runnable? = null,
-    ) {
-        setTextStyleInternal(
-            fvar =
-                fontVariationUtils.updateFontVariation(
-                    weight = weight,
-                    width = width,
-                    opticalSize = opticalSize,
-                    roundness = roundness,
-                ),
-            textSize = textSize,
-            color = color,
-            strokeWidth = strokeWidth,
-            animate = animate,
-            duration = duration,
-            interpolator = interpolator,
-            delay = delay,
-            onAnimationEnd = onAnimationEnd,
-            updateLayoutOnFailure = true,
-        )
-    }
-
     companion object {
         private val TAG = TextAnimator::class.simpleName!!
+        const val DEFAULT_ANIMATION_DURATION = 300L
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
index 58c2a1c..86c7f76 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
@@ -24,14 +24,14 @@
  * A registry to temporarily store the view being transitioned into a Dialog (using
  * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator])
  */
-class ViewTransitionRegistry {
+class ViewTransitionRegistry : IViewTransitionRegistry {
 
     /**
      * A map of a unique token to a WeakReference of the View being transitioned. WeakReference
      * ensures that Views are garbage collected whenever they become eligible and avoid any
      * memory leaks
      */
-    private val registry by lazy {  mutableMapOf<ViewTransitionToken, WeakReference<View>>() }
+    private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() }
 
     /**
      * A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to
@@ -45,8 +45,7 @@
             }
 
             override fun onViewDetachedFromWindow(view: View) {
-                (view.getTag(R.id.tag_view_transition_token)
-                        as? ViewTransitionToken)?.let { token -> unregister(token) }
+                getViewToken(view)?.let { token -> unregister(token) }
             }
         }
     }
@@ -57,12 +56,12 @@
      * @param token unique token associated with the transitioning view
      * @param view view undergoing transitions
      */
-    fun register(token: ViewTransitionToken, view: View) {
+    override fun register(token: ViewTransitionToken, view: View) {
         // token embedded as a view tag enables to use a single listener for all views
         view.setTag(R.id.tag_view_transition_token, token)
         view.addOnAttachStateChangeListener(listener)
         registry[token] = WeakReference(view)
-        emitCountForTrace()
+        onRegistryUpdate()
     }
 
     /**
@@ -70,30 +69,51 @@
      *
      * @param token unique token associated with the transitioning view
      */
-    fun unregister(token: ViewTransitionToken) {
+    override fun unregister(token: ViewTransitionToken) {
         registry.remove(token)?.let {
             it.get()?.let { view ->
                 view.removeOnAttachStateChangeListener(listener)
                 view.setTag(R.id.tag_view_transition_token, null)
             }
             it.clear()
+            onRegistryUpdate()
         }
-        emitCountForTrace()
     }
 
     /**
      * Access a view from registry using unique "token" associated with it
      * WARNING - this returns a StrongReference to the View stored in the registry
      */
-    fun getView(token: ViewTransitionToken): View? {
+    override fun getView(token: ViewTransitionToken): View? {
         return registry[token]?.get()
     }
 
     /**
+     * Return token mapped to the [view], if it is present in the registry
+     *
+     * @param view the transitioning view whose token we are requesting
+     * @return token associated with the [view] if present, else null
+     */
+    override fun getViewToken(view: View): ViewTransitionToken? {
+        return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token ->
+            getView(token)?.let { token }
+        }
+    }
+
+    /** Event call to run on registry update (on both [register] and [unregister]) */
+    override fun onRegistryUpdate() {
+        emitCountForTrace()
+    }
+
+    /**
      * Utility function to emit number of non-null views in the registry whenever the registry is
      * updated (via [register] or [unregister])
      */
     private fun emitCountForTrace() {
         Trace.setCounter("transition_registry_view_count", registry.count().toLong())
     }
+
+    companion object {
+        val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() }
+    }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
index c211a8e..e011df0 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
@@ -16,17 +16,19 @@
 
 package com.android.systemui.animation
 
+import java.util.UUID
+
 /**
  * A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be
  * unique as timestamp is appended to the token string
  *
- * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or
- * "ClassName_timestamp"
+ * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or
+ * "ClassName_UUID"
  *
  * @property token String value of a unique token
  */
 @JvmInline
 value class ViewTransitionToken private constructor(val token: String) {
-    constructor() : this(token = System.currentTimeMillis().toString())
-    constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}")
+    constructor() : this(token = UUID.randomUUID().toString())
+    constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}")
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 0b17a3f..9b76f15 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -42,6 +42,7 @@
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.transitions
@@ -87,10 +88,26 @@
         spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
         fade(AllElements)
     }
+    to(CommunalScenes.Communal, key = CommunalTransitionKeys.Swipe) {
+        spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
+        translate(Communal.Elements.Grid, Edge.End)
+        timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+    }
     to(CommunalScenes.Blank) {
         spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
         fade(AllElements)
     }
+    to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) {
+        spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
+        translate(Communal.Elements.Grid, Edge.End)
+        timestampRange(endMillis = 167) {
+            fade(Communal.Elements.Grid)
+            fade(Communal.Elements.IndicationArea)
+            fade(Communal.Elements.LockIcon)
+            fade(Communal.Elements.StatusBar)
+        }
+        timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) }
+    }
 }
 
 val sceneTransitions = transitions {
@@ -165,6 +182,7 @@
         viewModel.communalBackground.collectAsStateWithLifecycle(
             initialValue = CommunalBackgroundType.ANIMATED
         )
+    val swipeToHubEnabled by viewModel.swipeToHubEnabled.collectAsStateWithLifecycle()
     val state: MutableSceneTransitionLayoutState =
         rememberMutableSceneTransitionLayoutState(
             initialScene = currentSceneKey,
@@ -200,15 +218,27 @@
         scene(
             CommunalScenes.Blank,
             userActions =
-                if (viewModel.swipeToHubEnabled())
-                    mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal)
-                else emptyMap(),
+                if (swipeToHubEnabled) {
+                    mapOf(
+                        Swipe.Start(fromSource = Edge.End) to
+                            UserActionResult(CommunalScenes.Communal, CommunalTransitionKeys.Swipe)
+                    )
+                } else {
+                    emptyMap()
+                },
         ) {
             // This scene shows nothing only allowing for transitions to the communal scene.
             Box(modifier = Modifier.fillMaxSize())
         }
 
-        scene(CommunalScenes.Communal, userActions = mapOf(Swipe.End to CommunalScenes.Blank)) {
+        scene(
+            CommunalScenes.Communal,
+            userActions =
+                mapOf(
+                    Swipe.End to
+                        UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe)
+                ),
+        ) {
             CommunalScene(
                 backgroundType = backgroundType,
                 colors = colors,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 64f3cb1..297995b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalResources
 import androidx.compose.ui.res.dimensionResource
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.ElementKey
@@ -34,6 +34,13 @@
 import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.isLandscape
+import com.android.systemui.media.controls.ui.controller.MediaCarouselController
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
+import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
+import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
 import com.android.systemui.res.R
@@ -42,10 +49,11 @@
 import com.android.systemui.scene.ui.composable.Overlay
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.composable.OverlayShadeHeader
-import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.util.Utils
 import dagger.Lazy
 import javax.inject.Inject
+import javax.inject.Named
 import kotlinx.coroutines.flow.Flow
 
 @SysUISingleton
@@ -58,6 +66,8 @@
     private val stackScrollView: Lazy<NotificationScrollView>,
     private val clockSection: DefaultClockSection,
     private val keyguardClockViewModel: KeyguardClockViewModel,
+    private val mediaCarouselController: MediaCarouselController,
+    @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>,
 ) : Overlay {
     override val key = Overlays.NotificationsShade
 
@@ -84,6 +94,11 @@
                 viewModel.notificationsPlaceholderViewModelFactory.create()
             }
 
+        val usingCollapsedLandscapeMedia =
+            Utils.useCollapsedMediaInLandscape(LocalResources.current)
+        mediaHost.get().expansion =
+            if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED
+
         OverlayShade(
             panelElement = NotificationsShade.Elements.Panel,
             alignmentOnWideScreens = Alignment.TopStart,
@@ -96,9 +111,7 @@
                     }
                 OverlayShadeHeader(
                     viewModel = headerViewModel,
-                    modifier =
-                        Modifier.element(NotificationsShade.Elements.StatusBar)
-                            .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
+                    modifier = Modifier.element(NotificationsShade.Elements.StatusBar),
                 )
             },
         ) {
@@ -116,6 +129,19 @@
                         }
                     }
 
+                    MediaCarousel(
+                        isVisible = viewModel.showMedia,
+                        mediaHost = mediaHost.get(),
+                        carouselController = mediaCarouselController,
+                        usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
+                        modifier =
+                            Modifier.padding(
+                                top = notificationStackPadding,
+                                start = notificationStackPadding,
+                                end = notificationStackPadding,
+                            ),
+                    )
+
                     NotificationScrollingStack(
                         shadeSession = shadeSession,
                         stackScrollView = stackScrollView.get(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index afdb3cb..da0b8ac 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
@@ -66,7 +65,6 @@
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.composable.OverlayShadeHeader
 import com.android.systemui.shade.ui.composable.QuickSettingsOverlayHeader
-import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -108,6 +106,11 @@
             rememberViewModel("QuickSettingsShadeOverlayContainer") {
                 quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = true)
             }
+        val hunPlaceholderViewModel =
+            rememberViewModel("QuickSettingsShadeOverlayPlaceholder") {
+                notificationsPlaceholderViewModelFactory.create()
+            }
+
         val panelCornerRadius =
             with(LocalDensity.current) { OverlayShade.Dimensions.PanelCornerRadius.toPx().toInt() }
         val showBrightnessMirror =
@@ -119,13 +122,6 @@
         DisposableEffect(Unit) { onDispose { contentViewModel.onPanelShapeChanged(null) } }
 
         Box(modifier = modifier.graphicsLayer { alpha = contentAlphaFromBrightnessMirror }) {
-            SnoozeableHeadsUpNotificationSpace(
-                stackScrollView = notificationStackScrollView.get(),
-                viewModel =
-                    rememberViewModel("QuickSettingsShadeOverlayPlaceholder") {
-                        notificationsPlaceholderViewModelFactory.create()
-                    },
-            )
             OverlayShade(
                 panelElement = QuickSettingsShade.Elements.Panel,
                 alignmentOnWideScreens = Alignment.TopEnd,
@@ -133,50 +129,34 @@
                 header = {
                     OverlayShadeHeader(
                         viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
-                        modifier =
-                            Modifier.element(QuickSettingsShade.Elements.StatusBar)
-                                .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
+                        modifier = Modifier.element(QuickSettingsShade.Elements.StatusBar),
                     )
                 },
             ) {
-                ShadeBody(
+                QuickSettingsContainer(
                     viewModel = quickSettingsContainerViewModel,
                     modifier =
                         Modifier.onPlaced { coordinates ->
-                            val boundsInWindow = coordinates.boundsInWindow()
-                            val shadeScrimBounds =
-                                ShadeScrimBounds(
-                                    left = boundsInWindow.left,
-                                    top = boundsInWindow.top,
-                                    right = boundsInWindow.right,
-                                    bottom = boundsInWindow.bottom,
-                                )
                             val shape =
                                 ShadeScrimShape(
-                                    bounds = shadeScrimBounds,
+                                    bounds = ShadeScrimBounds(coordinates.boundsInWindow()),
                                     topRadius = 0,
                                     bottomRadius = panelCornerRadius,
                                 )
                             contentViewModel.onPanelShapeChanged(shape)
                         },
-                    header = {
-                        if (quickSettingsContainerViewModel.showHeader) {
-                            QuickSettingsOverlayHeader(
-                                viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
-                                modifier =
-                                    Modifier.element(QuickSettingsShade.Elements.Header)
-                                        .padding(top = QuickSettingsShade.Dimensions.Padding),
-                            )
-                        }
-                    },
                 )
             }
+            SnoozeableHeadsUpNotificationSpace(
+                stackScrollView = notificationStackScrollView.get(),
+                viewModel = hunPlaceholderViewModel,
+            )
         }
     }
 }
 
-// The possible states of the `ShadeBody`.
-sealed interface ShadeBodyState {
+/** The possible states of the `ShadeBody`. */
+private sealed interface ShadeBodyState {
     data object Editing : ShadeBodyState
 
     data object TileDetails : ShadeBodyState
@@ -185,10 +165,9 @@
 }
 
 @Composable
-fun ContentScope.ShadeBody(
+fun ContentScope.QuickSettingsContainer(
     viewModel: QuickSettingsContainerViewModel,
     modifier: Modifier = Modifier,
-    header: @Composable () -> Unit,
 ) {
     val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle()
     val tileDetails =
@@ -216,11 +195,10 @@
                 TileDetails(modifier = modifier, viewModel.detailsViewModel)
             }
 
-            else -> {
+            ShadeBodyState.Default -> {
                 QuickSettingsLayout(
                     viewModel = viewModel,
                     modifier = modifier.sysuiResTag("quick_settings_panel"),
-                    header = header,
                 )
             }
         }
@@ -232,7 +210,6 @@
 fun ContentScope.QuickSettingsLayout(
     viewModel: QuickSettingsContainerViewModel,
     modifier: Modifier = Modifier,
-    header: @Composable () -> Unit,
 ) {
     Column(
         verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding),
@@ -244,7 +221,14 @@
                 bottom = QuickSettingsShade.Dimensions.Padding,
             ),
     ) {
-        header()
+        if (viewModel.showHeader) {
+            QuickSettingsOverlayHeader(
+                viewModel = viewModel.shadeHeaderViewModel,
+                modifier =
+                    Modifier.element(QuickSettingsShade.Elements.Header)
+                        .padding(top = QuickSettingsShade.Dimensions.Padding),
+            )
+        }
         Toolbar(
             modifier =
                 Modifier.fillMaxWidth().requiredHeight(QuickSettingsShade.Dimensions.ToolbarHeight),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
index 8aa5bc7..60eaa28 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -21,6 +21,7 @@
 import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
+import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.systemui.scene.shared.model.SceneDataSource
 import kotlinx.coroutines.CoroutineScope
@@ -103,4 +104,8 @@
     override fun instantlyHideOverlay(overlay: OverlayKey) {
         state.snapTo(overlays = state.currentOverlays - overlay)
     }
+
+    override fun freezeAndAnimateToCurrentState() {
+        (state.transitionState as? TransitionState.Transition)?.freezeAndAnimateToCurrentState()
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index fffc7f9..2d2a8154 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -16,6 +16,8 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MotionScheme
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -59,4 +61,8 @@
 
     override val layoutDirection: LayoutDirection
         get() = layoutImpl.layoutDirection
+
+    @ExperimentalMaterial3ExpressiveApi
+    override val motionScheme: MotionScheme
+        get() = layoutImpl.state.motionScheme
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index e0b4218..613afe2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -18,7 +18,8 @@
 
 import androidx.compose.animation.core.Easing
 import androidx.compose.animation.core.LinearEasing
-import androidx.compose.ui.geometry.Offset
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MotionScheme
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -29,8 +30,8 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.ElementStateScope
-import com.android.compose.animation.scene.Scale
 import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.PropertyTransformation.Property
 import kotlinx.coroutines.CoroutineScope
 
 /** A transformation applied to one or more elements during a transition. */
@@ -126,9 +127,13 @@
     ): T
 }
 
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 interface PropertyTransformationScope : Density, ElementStateScope {
     /** The current [direction][LayoutDirection] of the layout. */
     val layoutDirection: LayoutDirection
+
+    /** The [MotionScheme] in use by the [SceneTransitionLayout]. */
+    val motionScheme: MotionScheme
 }
 
 /** Defines the transformation-type to be applied to all elements matching [matcher]. */
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index b76656d..4bf0ceb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -366,7 +366,7 @@
 
     fun animateCharge(isDozing: () -> Boolean) {
         // Skip charge animation if dozing animation is already playing.
-        if (textAnimator == null || textAnimator!!.isRunning()) {
+        if (textAnimator == null || textAnimator!!.isRunning) {
             return
         }
 
@@ -444,29 +444,28 @@
         delay: Long,
         onAnimationEnd: Runnable?,
     ) {
-        textAnimator?.let {
-            it.setTextStyle(
-                weight = weight,
-                color = color,
+        val style = TextAnimator.Style(color = color)
+        val animation =
+            TextAnimator.Animation(
                 animate = animate && isAnimationEnabled,
                 duration = duration,
-                interpolator = interpolator,
-                delay = delay,
+                interpolator = interpolator ?: Interpolators.LINEAR,
+                startDelay = delay,
                 onAnimationEnd = onAnimationEnd,
             )
+        textAnimator?.let {
+            it.setTextStyle(
+                style.withUpdatedFVar(it.fontVariationUtils, weight = weight),
+                animation,
+            )
             it.glyphFilter = glyphFilter
         }
             ?: run {
                 // when the text animator is set, update its start values
                 onTextAnimatorInitialized = { textAnimator ->
                     textAnimator.setTextStyle(
-                        weight = weight,
-                        color = color,
-                        animate = false,
-                        duration = duration,
-                        interpolator = interpolator,
-                        delay = delay,
-                        onAnimationEnd = onAnimationEnd,
+                        style.withUpdatedFVar(textAnimator.fontVariationUtils, weight = weight),
+                        animation.copy(animate = false),
                     )
                     textAnimator.glyphFilter = glyphFilter
                 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index a5adfa2..0b7ea1a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -21,6 +21,7 @@
 import android.view.animation.Interpolator
 import android.widget.RelativeLayout
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.TextAnimator
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
 import com.android.systemui.plugins.clocks.AlarmData
@@ -65,7 +66,7 @@
 data class FontTextStyle(
     val lineHeight: Float? = null,
     val fontSizeScale: Float? = null,
-    val transitionDuration: Long = -1L,
+    val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION,
     val transitionInterpolator: Interpolator? = null,
 )
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 92fa6b5..8317aa3 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -33,7 +33,9 @@
 import android.util.TypedValue
 import android.view.View.MeasureSpec.EXACTLY
 import android.view.animation.Interpolator
+import android.view.animation.PathInterpolator
 import android.widget.TextView
+import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.GSFAxes
 import com.android.systemui.animation.TextAnimator
@@ -84,17 +86,20 @@
             else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
         }
 
-    private var lsFontVariation =
-        if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar()
-        else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar()
+    private var lsFontVariation: String
+    private var aodFontVariation: String
+    private var fidgetFontVariation: String
 
-    private var aodFontVariation = run {
+    init {
         val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS
-        (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
-    }
+        val lsFontAxes =
+            if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS)
+            else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS)
 
-    // TODO(b/374306512): Fidget endpoint to spec
-    private var fidgetFontVariation = aodFontVariation
+        lsFontVariation = lsFontAxes.toFVar()
+        aodFontVariation = (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
+        fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
+    }
 
     private val parser = DimensionParser(clockCtx.context)
     var maxSingleDigitHeight = -1
@@ -121,7 +126,7 @@
     protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: ClockLogger.INIT_LOGGER
 
-    private var aodDozingInterpolator: Interpolator? = null
+    private var aodDozingInterpolator: Interpolator = Interpolators.LINEAR
 
     @VisibleForTesting lateinit var textAnimator: TextAnimator
 
@@ -149,7 +154,7 @@
         lockscreenColor = color
         lockScreenPaint.color = lockscreenColor
         if (dozeFraction < 1f) {
-            textAnimator.setTextStyle(color = lockscreenColor, animate = false)
+            textAnimator.setTextStyle(TextAnimator.Style(color = lockscreenColor))
         }
         invalidate()
     }
@@ -157,10 +162,8 @@
     fun updateAxes(lsAxes: List<ClockFontAxisSetting>) {
         lsFontVariation = lsAxes.toFVar()
         aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar()
-        logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) {
-            str1 = lsFontVariation
-            str2 = aodFontVariation
-        }
+        fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar()
+        logger.updateAxes(lsFontVariation, aodFontVariation)
 
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
         typeface = lockScreenPaint.typeface
@@ -168,13 +171,28 @@
         lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
         targetTextBounds.set(textBounds)
 
-        textAnimator.setTextStyle(fvar = lsFontVariation, animate = false)
+        textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
         recomputeMaxSingleDigitSizes()
         requestLayout()
         invalidate()
     }
 
+    fun buildFidgetVariation(axes: List<ClockFontAxisSetting>): List<ClockFontAxisSetting> {
+        val result = mutableListOf<ClockFontAxisSetting>()
+        for (axis in axes) {
+            result.add(
+                FIDGET_DISTS.get(axis.key)?.let { (dist, midpoint) ->
+                    ClockFontAxisSetting(
+                        axis.key,
+                        axis.value + dist * if (axis.value > midpoint) -1 else 1,
+                    )
+                } ?: axis
+            )
+        }
+        return result
+    }
+
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.onMeasure()
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
@@ -245,40 +263,54 @@
 
     fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
         if (!this::textAnimator.isInitialized) return
+        logger.animateDoze()
         textAnimator.setTextStyle(
-            animate = isAnimated && isAnimationEnabled,
-            color = if (isDozing) AOD_COLOR else lockscreenColor,
-            textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
-            fvar = if (isDozing) aodFontVariation else lsFontVariation,
-            duration = aodStyle.transitionDuration,
-            interpolator = aodDozingInterpolator,
+            TextAnimator.Style(
+                fVar = if (isDozing) aodFontVariation else lsFontVariation,
+                color = if (isDozing) AOD_COLOR else lockscreenColor,
+                textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+            ),
+            TextAnimator.Animation(
+                animate = isAnimated && isAnimationEnabled,
+                duration = aodStyle.transitionDuration,
+                interpolator = aodDozingInterpolator,
+            ),
         )
         updateTextBoundsForTextAnimator()
     }
 
     fun animateCharge() {
-        if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+        if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        logger.d("animateCharge()")
+        logger.animateCharge()
+
+        val lsStyle = TextAnimator.Style(fVar = lsFontVariation)
+        val aodStyle = TextAnimator.Style(fVar = aodFontVariation)
+
         textAnimator.setTextStyle(
-            fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation,
-            animate = isAnimationEnabled,
-            onAnimationEnd =
-                Runnable {
+            if (dozeFraction == 0f) aodStyle else lsStyle,
+            TextAnimator.Animation(
+                animate = isAnimationEnabled,
+                duration = CHARGE_ANIMATION_DURATION,
+                onAnimationEnd = {
                     textAnimator.setTextStyle(
-                        fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
-                        animate = isAnimationEnabled,
+                        if (dozeFraction == 0f) lsStyle else aodStyle,
+                        TextAnimator.Animation(
+                            animate = isAnimationEnabled,
+                            duration = CHARGE_ANIMATION_DURATION,
+                        ),
                     )
                     updateTextBoundsForTextAnimator()
                 },
+            ),
         )
         updateTextBoundsForTextAnimator()
     }
 
     fun animateFidget(x: Float, y: Float) {
-        if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+        if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
             // Skip fidget animation if other animation is already playing.
             return
         }
@@ -286,19 +318,25 @@
         logger.animateFidget(x, y)
         clockCtx.vibrator?.vibrate(FIDGET_HAPTICS)
 
-        // TODO(b/374306512): Duplicated charge animation as placeholder. Implement final version
-        // when we have a complete spec. May require additional code to animate individual digits.
+        // TODO(b/374306512): Delay each glyph's animation based on x/y position
         textAnimator.setTextStyle(
-            fvar = fidgetFontVariation,
-            animate = isAnimationEnabled,
-            onAnimationEnd =
-                Runnable {
+            TextAnimator.Style(fVar = fidgetFontVariation),
+            TextAnimator.Animation(
+                animate = isAnimationEnabled,
+                duration = FIDGET_ANIMATION_DURATION,
+                interpolator = FIDGET_INTERPOLATOR,
+                onAnimationEnd = {
                     textAnimator.setTextStyle(
-                        fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
-                        animate = isAnimationEnabled,
+                        TextAnimator.Style(fVar = lsFontVariation),
+                        TextAnimator.Animation(
+                            animate = isAnimationEnabled,
+                            duration = FIDGET_ANIMATION_DURATION,
+                            interpolator = FIDGET_INTERPOLATOR,
+                        ),
                     )
                     updateTextBoundsForTextAnimator()
                 },
+            ),
         )
         updateTextBoundsForTextAnimator()
     }
@@ -329,42 +367,20 @@
     }
 
     private fun getInterpolatedTextBounds(): Rect {
-        val interpolatedTextBounds = Rect()
-        if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) {
-            interpolatedTextBounds.left =
-                MathUtils.lerp(
-                        prevTextBounds.left,
-                        targetTextBounds.left,
-                        textAnimator.animator.animatedValue as Float,
-                    )
-                    .toInt()
-
-            interpolatedTextBounds.right =
-                MathUtils.lerp(
-                        prevTextBounds.right,
-                        targetTextBounds.right,
-                        textAnimator.animator.animatedValue as Float,
-                    )
-                    .toInt()
-
-            interpolatedTextBounds.top =
-                MathUtils.lerp(
-                        prevTextBounds.top,
-                        targetTextBounds.top,
-                        textAnimator.animator.animatedValue as Float,
-                    )
-                    .toInt()
-
-            interpolatedTextBounds.bottom =
-                MathUtils.lerp(
-                        prevTextBounds.bottom,
-                        targetTextBounds.bottom,
-                        textAnimator.animator.animatedValue as Float,
-                    )
-                    .toInt()
-        } else {
-            interpolatedTextBounds.set(targetTextBounds)
+        val progress = textAnimator.animator?.let { it.animatedValue as Float } ?: 1f
+        if (!textAnimator.isRunning || progress >= 1f) {
+            return Rect(targetTextBounds)
         }
+
+        val interpolatedTextBounds = Rect()
+        interpolatedTextBounds.left =
+            MathUtils.lerp(prevTextBounds.left, targetTextBounds.left, progress).toInt()
+        interpolatedTextBounds.right =
+            MathUtils.lerp(prevTextBounds.right, targetTextBounds.right, progress).toInt()
+        interpolatedTextBounds.top =
+            MathUtils.lerp(prevTextBounds.top, targetTextBounds.top, progress).toInt()
+        interpolatedTextBounds.bottom =
+            MathUtils.lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress).toInt()
         return interpolatedTextBounds
     }
 
@@ -471,7 +487,7 @@
         textStyle.lineHeight?.let { lineHeight = it.toInt() }
 
         this.aodStyle = aodStyle ?: textStyle.copy()
-        this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it }
+        aodDozingInterpolator = this.aodStyle.transitionInterpolator ?: Interpolators.LINEAR
         lockScreenPaint.strokeWidth = textBorderWidth
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
         setInterpolatorPaint()
@@ -500,7 +516,7 @@
         recomputeMaxSingleDigitSizes()
 
         if (this::textAnimator.isInitialized) {
-            textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+            textAnimator.setTextStyle(TextAnimator.Style(textSize = lockScreenPaint.textSize))
         }
     }
 
@@ -525,10 +541,11 @@
             textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
             textAnimator.textInterpolator.onTargetPaintModified()
             textAnimator.setTextStyle(
-                fvar = lsFontVariation,
-                textSize = lockScreenPaint.textSize,
-                color = lockscreenColor,
-                animate = false,
+                TextAnimator.Style(
+                    fVar = lsFontVariation,
+                    textSize = lockScreenPaint.textSize,
+                    color = lockscreenColor,
+                )
             )
         }
     }
@@ -572,6 +589,17 @@
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43)
                 .compose()
 
+        val CHARGE_ANIMATION_DURATION = 500L
+        val FIDGET_ANIMATION_DURATION = 250L
+        val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f)
+        val FIDGET_DISTS =
+            mapOf(
+                GSFAxes.WEIGHT to Pair(200f, 500f),
+                GSFAxes.WIDTH to Pair(30f, 75f),
+                GSFAxes.ROUND to Pair(0f, 50f),
+                GSFAxes.SLANT to Pair(0f, -5f),
+            )
+
         val AOD_COLOR = Color.WHITE
         val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f)
         val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f)
diff --git a/packages/SystemUI/docs/qs-tiles.md b/packages/SystemUI/docs/qs-tiles.md
index ee388ec..87d9b7c 100644
--- a/packages/SystemUI/docs/qs-tiles.md
+++ b/packages/SystemUI/docs/qs-tiles.md
@@ -8,6 +8,14 @@
 Settings tiles. It provides descriptions about the lifecycle of a tile, how to create new tiles and
 how SystemUI manages and displays tiles, among other topics.
 
+A lot of the tile backend architecture is in the process of being replaced by a new architecture in
+order to align with the
+[recommended architecture](https://developer.android.com/topic/architecture#recommended-app-arch).
+
+While we are in the process of migrating, this document will try to provide a comprehensive
+overview of the current architecture as well as the new one. The sections documenting the new
+architecture are marked with the tag [NEW-ARCH].
+
 ## What are Quick Settings Tiles?
 
 Quick Settings (from now on, QS) is the expanded panel that contains shortcuts for the user to
@@ -72,6 +80,27 @@
 implement plugins to add additional tiles or different behavior. For more information,
 see [plugins.md](plugins.md)
 
+
+#### [NEW-ARCH] Tile backend
+Instead of `QSTileImpl` the tile backend is made of a view model called `QSTileViewModelImpl`,
+which in turn is composed of 3 interfaces:
+
+* [`QSTileDataInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt)
+is responsible for providing the data for the tile. It is responsible for fetching the state of
+the tile from the source of truth and providing that information to the tile. Typically the data
+interactor will read system state from a repository or a controller and provide a flow of
+domain-specific data model.
+
+* [`QSTileUserActionInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt) is responsible for handling the user actions on the tile.
+This interactor decides what should happen when the user clicks, long clicks on the tile.
+
+* [`QSTileDataToStateMapper`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/mapper/QSTileMapper.kt)
+is responsible for mapping the data received from the data interactor to a state that the view
+model can use to update the UI.
+
+At the time being, the `QSTileViewModel`s are adapted to `QSTile`. This conversion is done by
+`QSTileViewModelAdapter`.
+
 #### Tile State
 
 Each tile has an associated `State` object that is used to communicate information to the
@@ -94,6 +123,11 @@
 to `state == Tile#STATE_ACTIVE`. This is used by accessibility services along
 with `expandedAccessibilityClassName`.
 
+#### [NEW-ARCH] Tile State
+In the new architecture, the mapper generates
+[`QSTileState`](packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt),
+which again is converted to the old state by `QSTileViewModelAdapter`.
+
 #### SystemUI tiles
 
 Each tile defined in SystemUI extends `QSTileImpl`. This abstract class implements some common
@@ -103,6 +137,9 @@
 For more information on how to implement a tile in SystemUI,
 see [Implementing a SystemUI tile](#implementing-a-systemui-tile).
 
+As mentioned before, when the [NEW-ARCH] migration is complete, we will remove the `QSTileImpl`
+and `QSTileViewModelAdapter` and directly use`QSTileViewModelImpl`.
+
 ### Tile views
 
 Each Tile has a couple of associated views for displaying it in QS and QQS. These views are updated
@@ -154,37 +191,24 @@
 #### Life of a tile click
 
 This is a brief run-down of what happens when a user clicks on a tile. Internal changes on the
-device (for example, changes from Settings) will trigger this process starting in step 3. Throughout
-this section, we assume that we are dealing with a `QSTileImpl`.
+device (for example, changes from Settings) will trigger this process starting in step 5.
 
-1. User clicks on tile. The following calls happen in sequence:
-    1. `QSTileViewImpl#onClickListener`.
-    2. `QSTile#click`.
-    3. `QSTileImpl#handleClick`. This last call sets the new state for the device by using the
-       associated controller.
-2. State in the device changes. This is normally outside of SystemUI's control.
-3. Controller receives a callback (or `Intent`) indicating the change in the device. The following
-   calls happen:
-    1. `QSTileImpl#refreshState`, maybe passing an object with necessary information regarding the
-       new state.
-    2. `QSTileImpl#handleRefreshState`
-4. `QSTileImpl#handleUpdateState` is called to update the state with the new information. This
-   information can be obtained both from the `Object` passed to `refreshState` as well as from the
-   controller.
-5. If the state has changed (in at least one element), `QSTileImpl#handleStateChanged` is called.
-   This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the
-   new `State`.
-6. `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method
-   maps the state into the view:
-    * The tile colors change to match the new state.
-    * `QSIconView.setIcon` is called to apply the correct state to the icon and the correct icon to
-      the view.
-    * The tile labels change to match the new state.
+Step | Legacy Tiles | [NEW-ARCH] Tiles
+-------|-------|---------
+1 | User clicks on tile. | Same as legacy tiles.
+2 | `QSTileViewImpl#onClickListener` | Same as legacy tiles.
+3 | `QSTile#click` | Same as legacy tiles.
+4| `QSTileImpl#handleClick` | `QSTileUserActionInteractor#handleInput`
+5| State in the device changes. This is normally outside of SystemUI's control. Controller receives a callback (or `Intent`) indicating the change in the device. | Same as legacy tiles.
+6 |  `QSTile#refreshState`and `QSTileImpl#handleRefreshState` | `QSTileDataInteractor#tileData()`
+7| `QSTileImpl#handleUpdateState` is called to update the state with the new information. This information can be obtained both from the `Object` passed to `refreshState` as well as from the controller. | The data that was generated by the data interactor is read by the `QSTileViewModelImpl.state` flow which calls `QSTileMapper#map` on the data to generate a new `QSTileState`.
+8|  If the state has changed (in at least one element `QSTileImpl#handleStateChanged` is called. This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the new `State`. | The newly mapped QSTileState is read by the `QSTileViewModelAdapter` which then maps it to a legacy `State`. Similarly to the legacy tiles, the new state is compared to the old one and if there is a difference, `QSTile.Callback#onStateChanged` is called for all the associated callbacks.
+9 | `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method maps the state updating tile color and label, and calling `QSIconView.setIcon` | Same as legacy tiles.
 
 ## Third party tiles (TileService)
 
-A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI). This
-is implemented by developers
+A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI).
+This is implemented by developers
 subclassing [`TileService`](/core/java/android/service/quicksettings/TileService.java) and
 interacting with its API.
 
@@ -220,9 +244,9 @@
 * **`onTileAdded`**: called when the tile is added to QS.
 * **`onTileRemoved`**: called when the tile is removed from QS.
 * **`onStartListening`**: called when QS is opened and the tile is showing. This marks the start of
-  the window when calling `getQSTile` is safe and will provide the correct object.
-* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user. This
-  marks the end of the window described in `onStartListening`.
+the window when calling `getQSTile` is safe and will provide the correct object.
+* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user.
+This marks the end of the window described in `onStartListening`.
 * **`onClick`**: called when the user clicks on the tile.
 
 Additionally, the following final methods are provided:
@@ -379,13 +403,14 @@
 
 ### QSFactory
 
+`CurrentTilesInteractorImpl` uses the `QSFactory` interface to create the tiles.
+
 This interface provides a way of creating tiles and views from a spec. It can be used in plugins to
 provide different definitions for tiles.
 
-In SystemUI there is only one implementation of this factory and that is the default
-factory (`QSFactoryImpl`) in `CurrentTilesInteractorImpl`.
+In SystemUI there are two implementation of this factory. The first one is `QSFactoryImpl` in used for legacy tiles. The second one is `NewQSFactory` used for [NEW-ARCH] tiles.
 
-#### QSFactoryImpl
+#### QSFactoryImpl (legacy tiles)
 
 This class implements the following method as specified in the `QSFactory` interface:
 
@@ -402,6 +427,12 @@
   As part of filtering not valid tiles, custom tiles that don't have a corresponding valid service
   component are never instantiated.
 
+#### NewQSFactory ([NEW-ARCH] tiles)
+
+This class also implements the `createTile` method as specified in the `QSFactory` interface.
+However, it first uses the spec to get a `QSTileViewModel`. The view model is then adapted into a
+`QSTile` using the `QSTileViewModelAdapter`.
+
 ### Lifecycle of a Tile
 
 We describe first the parts of the lifecycle that are common to SystemUI tiles and third party
@@ -415,7 +446,7 @@
 2. This updates the flow that `CurrentTilesInteractor` is collecting from, triggering the process
    described above.
 3. `CurrentTilesInteractor` calls the available `QSFactory` classes in order to find one that will
-   be able to create a tile with that spec. Assuming that `QSFactoryImpl` managed to create the
+   be able to create a tile with that spec. Assuming that some factory managed to create the
    tile, which is some implementation of `QSTile` (either a SystemUI subclass
    of `QSTileImpl` or a `CustomTile`) it will be added to the current list.
    If the tile is available, it's stored in a map and things proceed forward.
@@ -452,7 +483,7 @@
 This section describes necessary and recommended steps when implementing a Quick Settings tile. Some
 of them are optional and depend on the requirements of the tile.
 
-### Implementing a SystemUI tile
+### Implementing a legacy SystemUI tile
 
 1. Create a class (preferably
    in [`SystemUI/src/com/android/systemui/qs/tiles`](/packages/SystemUI/src/com/android/systemui/qs/tiles))
@@ -579,6 +610,70 @@
   Provides a default label for this Tile. Used by the QS Panel customizer to show a name next to
   each available tile.
 
+### Implementing a [NEW-ARCH] SystemUI tile
+In the new system the tiles are created in the path
+[`packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>`](packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>)
+where the `<spec>` should be replaced by the spec of the tile e.g. rotation for `RotationLockTile`.
+
+To create a new tile, the developer needs to implement the following data class and interfaces:
+
+[`DataModel`] is a class that describes the system state of the feature that the tile is trying to
+represent. Let's refer to the type of this class as DATA_TYPE. For example a simplified version of
+the data model for a flashlight tile could be a class with a boolean field that represents
+whether the flashlight is on or not.
+
+This file should be placed in the relative path `domain/model/` down from the tile's package.
+
+[`QSTileDataInteractor`] There are two abstract methods that need to be implemented:
+* `fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>`: This method
+returns a flow of data that will be used to create the state of the tile. This is where the system
+state is listened to and converted to a flow of data model. Avoid loading data or settings up
+listeners on the main thread. The userHandle is the user for which the tile is created.
+The triggers flow is a flow of events that can be used to trigger a refresh of the data.
+The most common triggers the force update and initial request.
+
+* `fun availability(user: UserHandle): Flow<Boolean>`: This method returns a flow of booleans that
+indicates if the tile should be shown or not. This is where the availability of the system feature
+(e.g. wifi) is checked. The userHandle is the user for which the tile is created.
+
+This file should be placed in the relative path `domain/interactor/` down from the tile's package.
+
+[`QSTileUserActionInteractor`]
+* `fun handleInput(input: QSTileInput)` is the method that needs to be implemented. This is the
+method that will be called when the user interacts with the tile. The input parameter contains
+the type of interaction (click, long click, toggle) and the DATA_TYPE of the latest data when the
+input was received.
+
+This file should be placed in the relative path `/domain/interactor` down from the tile's package.
+
+[`QSTileDataToStateMapper`]
+* `fun map(data: DATA_TYPE): QSTileState` is the method that needs to be implemented. This method
+is responsible for mapping the data received from the data interactor to a state that the view
+model can use to update the UI. This is where for example the icon should be loaded, and the
+label and content description set. The map function will run on UIBackground thread, a single
+thread which has higher priority than the background thread and lower than UI thread. Loading a
+resource on UI thread can cause jank by blocking the UI thread. On the other end of the spectrum,
+loading resources using a background dispatcher may cause jank due to background thread contention
+since it is possible for the background dispatcher to use more than one background thread
+at the same time. In contrast, the UIBackground dispatcher uses a single thread that is
+shared by all tiles. Therefore the system will use UIBackground dispatcher to execute
+the map function.
+
+The most important resource to load in the map function is the icon. We prefer `Icon.Loaded` with a
+resource id over `Icon.Resource`, because then (a) we can guarantee that the drawable loading will
+happen on the UIBackground thread and (b) we can cache the drawables using the resource id.
+
+This file should be placed in the relative path `/ui/mapper` down from the tile's package.
+
+#### Testing a [NEW-ARCH] SystemUI tile
+
+When writing tests, the mapper is usually a good place to start, since that is where the
+business logic decisions are being made that can inform the shape of data interactor.
+
+We suggest taking advantage of the existing class `QSTileStateSubject`. So rather than
+asserting an individual field's value, a test will assert the whole state. That can be
+achieved by `QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)`.
+
 ### Implementing a third party tile
 
 For information about this, use the Android Developer documentation
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index bd81181..4140a95 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -18,9 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -28,8 +26,6 @@
 
 import android.hardware.biometrics.BiometricSourceType;
 import android.testing.TestableLooper;
-import android.text.Editable;
-import android.text.TextWatcher;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -99,19 +95,6 @@
     }
 
     @Test
-    public void textChanged_AnnounceForAccessibility() {
-        ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass(
-                TextWatcher.class);
-        mMessageAreaController.onViewAttached();
-        verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture());
-
-        textWatcherArgumentCaptor.getValue().afterTextChanged(
-                Editable.Factory.getInstance().newEditable("abc"));
-        verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class));
-        verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong());
-    }
-
-    @Test
     public void testSetBouncerVisible() {
         mMessageAreaController.setIsVisible(true);
         verify(mKeyguardMessageArea).setIsVisible(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index e4539b7..0b13900 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -45,7 +45,6 @@
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
-import android.os.Handler;
 import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 import android.testing.TestableLooper;
@@ -75,6 +74,8 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.After;
 import org.junit.Before;
@@ -107,6 +108,7 @@
     private static final String TEST_LABEL = "label";
     private static final int TEST_PRESET_INDEX = 1;
     private static final String TEST_PRESET_NAME = "test_preset";
+    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
     @Mock
     private SystemUIDialogManager mSystemUIDialogManager;
@@ -150,11 +152,9 @@
     private SystemUIDialog mDialog;
     private SystemUIDialog.Factory mDialogFactory;
     private HearingDevicesDialogDelegate mDialogDelegate;
-    private TestableLooper mTestableLooper;
 
     @Before
     public void setUp() {
-        mTestableLooper = TestableLooper.get(this);
         when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
         when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
@@ -186,6 +186,7 @@
     public void clickPairNewDeviceButton_intentActionMatch() {
         setUpDeviceDialogWithPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         getPairNewDeviceButton(mDialog).performClick();
 
@@ -232,6 +233,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         assertToolsUi(0);
     }
@@ -246,6 +248,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         assertToolsUi(1);
     }
@@ -267,6 +270,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         assertToolsUi(2);
     }
@@ -278,6 +282,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         ViewGroup presetLayout = getPresetLayout(mDialog);
         assertThat(presetLayout.getVisibility()).isEqualTo(View.GONE);
@@ -291,7 +296,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
 
         ViewGroup presetLayout = getPresetLayout(mDialog);
         assertThat(presetLayout.getVisibility()).isEqualTo(View.VISIBLE);
@@ -306,6 +311,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         ViewGroup ambientLayout = getAmbientLayout(mDialog);
         assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE);
@@ -318,6 +324,7 @@
 
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
 
         ViewGroup ambientLayout = getAmbientLayout(mDialog);
         assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE);
@@ -343,6 +350,7 @@
     public void onActiveDeviceChanged_presetExist_presetSelected() {
         setUpDeviceDialogWithoutPairNewDeviceButton();
         mDialog.show();
+        mExecutor.runAllReady();
         BluetoothHapPresetInfo info = getTestPresetInfo();
         when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info));
         when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(TEST_PRESET_INDEX);
@@ -351,7 +359,7 @@
         assertThat(spinner.getSelectedItemPosition()).isEqualTo(-1);
 
         mDialogDelegate.onActiveDeviceChanged(mCachedDevice, BluetoothProfile.LE_AUDIO);
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
 
         ViewGroup presetLayout = getPresetLayout(mDialog);
         assertThat(presetLayout.getVisibility()).isEqualTo(View.VISIBLE);
@@ -381,7 +389,8 @@
                 mActivityStarter,
                 mDialogTransitionAnimator,
                 mLocalBluetoothManager,
-                new Handler(mTestableLooper.getLooper()),
+                mExecutor,
+                mExecutor,
                 mAudioManager,
                 mUiEventLogger
         );
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
index d6ba98d..441f807 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -136,6 +137,118 @@
             assertThat(latest).isFalse()
         }
 
+    @Test
+    fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() =
+        kosmos.runTest {
+            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND)
+            fakeSystemClock.setCurrentTimeMillis(5000)
+
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000)
+        }
+
+    @Test
+    fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() =
+        kosmos.runTest {
+            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+            fakeSystemClock.setCurrentTimeMillis(5000)
+
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isNull()
+        }
+
+    @Test
+    fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() =
+        kosmos.runTest {
+            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+            fakeSystemClock.setCurrentTimeMillis(5000)
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isNull()
+
+            val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+            verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+            val listener = listenerCaptor.firstValue
+
+            // WHEN the app becomes visible
+            fakeSystemClock.setCurrentTimeMillis(7000)
+            listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)
+
+            // THEN the status and lastAppVisibleTime are updated
+            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)
+
+            // WHEN the app is no longer visible
+            listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING)
+
+            // THEN the lastAppVisibleTime is preserved
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)
+
+            // WHEN the app is visible again
+            fakeSystemClock.setCurrentTimeMillis(9000)
+            listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)
+
+            // THEN the lastAppVisibleTime is updated
+            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
+        }
+
+    @Test
+    fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() =
+        kosmos.runTest {
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+            verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+            val listener = listenerCaptor.firstValue
+
+            listener.onUidImportance(THIS_UID, IMPORTANCE_GONE)
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+
+            // WHEN another UID becomes foreground
+            listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND)
+
+            // THEN this UID still stays not visible
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+        }
+
+    @Test
+    fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() =
+        kosmos.runTest {
+            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+            whenever(activityManager.addOnUidImportanceListener(any(), any()))
+                .thenThrow(SecurityException())
+
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            // Verify no crash, and we get a value emitted
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+        }
+
+    /** Regression test for b/216248574. */
+    @Test
+    fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() =
+        kosmos.runTest {
+            whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException())
+
+            val latest by
+                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+            // Verify no crash, and we get a value emitted
+            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+        }
+
     companion object {
         private const val THIS_UID = 558
         private const val LOG_TAG = "LogTag"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
index e492c63..052d520 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
@@ -17,16 +17,20 @@
 package com.android.systemui.animation
 
 import android.os.HandlerThread
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import android.view.View
 import android.widget.FrameLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.view.LaunchableFrameLayout
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -40,6 +44,14 @@
     }
 
     private val interactionJankMonitor = FakeInteractionJankMonitor()
+    private lateinit var transitionRegistry: FakeViewTransitionRegistry
+    private lateinit var transitioningView: View
+
+    @Before
+    fun setup() {
+        transitioningView = LaunchableFrameLayout(mContext)
+        transitionRegistry = FakeViewTransitionRegistry()
+    }
 
     @Test
     fun animatingOrphanViewDoesNotCrash() {
@@ -67,7 +79,7 @@
         parent.addView((launchView))
         val launchController =
             GhostedViewTransitionAnimatorController(
-                    launchView,
+                launchView,
                 launchCujType = LAUNCH_CUJ,
                 returnCujType = RETURN_CUJ,
                 interactionJankMonitor = interactionJankMonitor
@@ -96,6 +108,26 @@
         assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ)
     }
 
+    @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
+    @Test
+    fun testViewsAreRegisteredInTransitionRegistry() {
+        GhostedViewTransitionAnimatorController(
+            transitioningView = transitioningView,
+            transitionRegistry = transitionRegistry
+        )
+        assertThat(transitionRegistry.registry).isNotEmpty()
+    }
+
+    @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
+    @Test
+    fun testNotUseRegistryIfDecouplingFlagDisabled() {
+        GhostedViewTransitionAnimatorController(
+            transitioningView = transitioningView,
+            transitionRegistry = transitionRegistry
+        )
+        assertThat(transitionRegistry.registry).isEmpty()
+    }
+
     /**
      * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and
      * allows inspection.
@@ -117,4 +149,30 @@
             return true
         }
     }
+
+    private class FakeViewTransitionRegistry : IViewTransitionRegistry {
+
+        val registry = mutableMapOf<ViewTransitionToken, View>()
+
+        override fun register(token: ViewTransitionToken, view: View) {
+            registry[token] = view
+            view.setTag(R.id.tag_view_transition_token, token)
+        }
+
+        override fun unregister(token: ViewTransitionToken) {
+            registry.remove(token)?.setTag(R.id.tag_view_transition_token, null)
+        }
+
+        override fun getView(token: ViewTransitionToken): View? {
+            return registry[token]
+        }
+
+        override fun getViewToken(view: View): ViewTransitionToken? {
+            return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken
+        }
+
+        override fun onRegistryUpdate() {
+            //empty
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 1a3606e..da25bca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -44,6 +45,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -65,7 +67,7 @@
 
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
     private val underTest: CommunalTransitionViewModel by lazy {
         kosmos.communalTransitionViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 329627a..e36d245 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
@@ -73,6 +74,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
@@ -80,21 +82,26 @@
     private val testScope: TestScope = kosmos.testScope
 
     private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor
+
     private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
     private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
     private val fakeUserRepository = kosmos.fakeUserRepository
     private val facePropertyRepository = kosmos.facePropertyRepository
-    private val fakeDeviceEntryFingerprintAuthInteractor =
-        kosmos.deviceEntryFingerprintAuthInteractor
-    private val powerInteractor = kosmos.powerInteractor
     private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
 
-    private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
+    private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor }
     private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
     private val trustManager = kosmos.trustManager
-    private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor
+
+    private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
+    private val fakeDeviceEntryFingerprintAuthInteractor by lazy {
+        kosmos.deviceEntryFingerprintAuthInteractor
+    }
+    private val powerInteractor by lazy { kosmos.powerInteractor }
+    private val deviceEntryFaceAuthStatusInteractor by lazy {
+        kosmos.deviceEntryFaceAuthStatusInteractor
+    }
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
index ff5fa39..7374f18 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt
@@ -37,7 +37,6 @@
 import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.hours
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,7 +50,6 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
-import org.mockito.kotlin.firstValue
 import org.mockito.kotlin.never
 import org.mockito.kotlin.secondValue
 import org.mockito.kotlin.verify
@@ -116,7 +114,6 @@
     fun showTouchpadNotification() = runTestAndClear {
         touchpadRepository.setIsAnyTouchpadConnected(true)
         testScope.advanceTimeBy(LAUNCH_DELAY)
-        mockExistingNotification()
         verifyNotification(
             R.string.launch_touchpad_tutorial_notification_title,
             R.string.launch_touchpad_tutorial_notification_content,
@@ -142,14 +139,10 @@
     }
 
     @Test
-    fun showKeyboardNotificationThenDisconnectKeyboard() = runTestAndClear {
+    fun cancelKeyboardNotificationWhenKeyboardDisconnects() = runTestAndClear {
         keyboardRepository.setIsAnyKeyboardConnected(true)
         testScope.advanceTimeBy(LAUNCH_DELAY)
-        verifyNotification(
-            R.string.launch_keyboard_tutorial_notification_title,
-            R.string.launch_keyboard_tutorial_notification_content,
-        )
-        mockExistingNotification()
+        mockNotifications(hasTutorialNotification = true)
 
         // After the keyboard is disconnected, i.e. there is nothing connected, the notification
         // should be cancelled
@@ -158,22 +151,71 @@
     }
 
     @Test
-    fun showKeyboardTouchpadNotificationThenDisconnectKeyboard() = runTestAndClear {
+    fun updateNotificationToTouchpadOnlyWhenKeyboardDisconnects() = runTestAndClear {
         keyboardRepository.setIsAnyKeyboardConnected(true)
         touchpadRepository.setIsAnyTouchpadConnected(true)
         testScope.advanceTimeBy(LAUNCH_DELAY)
-        mockExistingNotification()
+        mockNotifications(hasTutorialNotification = true)
+
         keyboardRepository.setIsAnyKeyboardConnected(false)
 
         verify(notificationManager, times(2))
             .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
-        // Connect both device and the first notification is for both
-        notificationCaptor.firstValue.verify(
+        // Connect both device and the first notification is for both. After the keyboard is
+        // disconnected, i.e. with only the touchpad left, the notification should be update to
+        // touchpad only
+        notificationCaptor.secondValue.verify(
+            R.string.launch_touchpad_tutorial_notification_title,
+            R.string.launch_touchpad_tutorial_notification_content,
+        )
+    }
+
+    @Test
+    fun updateNotificationToBothDevicesWhenTouchpadConnects() = runTestAndClear {
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+        mockNotifications(hasTutorialNotification = true)
+
+        touchpadRepository.setIsAnyTouchpadConnected(true)
+
+        verify(notificationManager, times(2))
+            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
+        // Update the notification from keyboard to both devices
+        notificationCaptor.secondValue.verify(
             R.string.launch_keyboard_touchpad_tutorial_notification_title,
             R.string.launch_keyboard_touchpad_tutorial_notification_content,
         )
-        // After the keyboard is disconnected, i.e. with only the touchpad left, the notification
-        // should be update to the one for only touchpad
+    }
+
+    @Test
+    fun doNotShowUpdateNotificationWhenInitialNotificationIsDismissed() = runTestAndClear {
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+        mockNotifications(hasTutorialNotification = false)
+
+        touchpadRepository.setIsAnyTouchpadConnected(true)
+
+        // There's only one notification being shown throughout this scenario. We don't update the
+        // notification because it has been dismissed when the touchpad connects
+        verifyNotification(
+            R.string.launch_keyboard_tutorial_notification_title,
+            R.string.launch_keyboard_tutorial_notification_content,
+        )
+    }
+
+    @Test
+    fun showTouchpadNotificationAfterDelayAndKeyboardNotificationIsDismissed() = runTestAndClear {
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+        mockNotifications(hasTutorialNotification = false)
+
+        touchpadRepository.setIsAnyTouchpadConnected(true)
+        testScope.advanceTimeBy(LAUNCH_DELAY)
+
+        verify(notificationManager, times(2))
+            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
+        // The keyboard notification was shown and dismissed; the touchpad notification is scheduled
+        // independently
         notificationCaptor.secondValue.verify(
             R.string.launch_touchpad_tutorial_notification_title,
             R.string.launch_touchpad_tutorial_notification_content,
@@ -189,10 +231,12 @@
             }
         }
 
-    // Assume that there's an existing notification when the updater checks activeNotifications
-    private fun mockExistingNotification() {
+    // Mock an active notification, so when the updater checks activeNotifications, it returns one
+    // with the given id. Otherwise, return an empty array (i.e. no active notifications)
+    private fun mockNotifications(hasTutorialNotification: Boolean) {
         whenever(notification.id).thenReturn(NOTIFICATION_ID)
-        whenever(notificationManager.activeNotifications).thenReturn(arrayOf(notification))
+        val notifications = if (hasTutorialNotification) arrayOf(notification) else emptyArray()
+        whenever(notificationManager.activeNotifications).thenReturn(notifications)
     }
 
     private fun verifyNotification(@StringRes titleResId: Int, @StringRes contentResId: Int) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index 183e4d6..98486a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -541,7 +541,7 @@
             simpleShortcutCategory(System, "System apps", "Take a note"),
             simpleShortcutCategory(System, "System controls", "Take screenshot"),
             simpleShortcutCategory(System, "System controls", "Go back"),
-            simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"),
+            simpleShortcutCategory(MultiTasking, "Split screen", "Use full screen"),
             simpleShortcutCategory(
                 MultiTasking,
                 "Split screen",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 29e95cd..0b42898 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -46,20 +47,31 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
-    val kosmos = testKosmos()
-    val underTest = kosmos.keyguardTransitionInteractor
-    val repository = kosmos.fakeKeyguardTransitionRepository
-    val testScope = kosmos.testScope
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var underTest: KeyguardTransitionInteractor
+
+    @Before
+    fun setup() {
+        repository = kosmos.fakeKeyguardTransitionRepository
+        underTest = kosmos.keyguardTransitionInteractor
+    }
 
     @Test
     fun transitionCollectorsReceivesOnlyAppropriateEvents() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
index 26fe379..3cff0fc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -1358,6 +1358,45 @@
             )
         }
 
+    /**
+     * When a transition away from the lockscreen is interrupted by an `Idle(Lockscreen)`, a
+     * `sceneState` that was set during the transition is consumed and passed to KTF.
+     */
+    @Test
+    fun transition_from_ls_scene_sceneStateSet_then_interrupted_by_idle_on_ls() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Gone,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false),
+                )
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            val sceneState = KeyguardState.AOD
+            underTest.onSceneAboutToChange(toScene = Scenes.Lockscreen, sceneState = sceneState)
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.AOD,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
     private fun assertTransition(
         step: TransitionStep,
         from: KeyguardState? = null,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt
deleted file mode 100644
index 052dfd5..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2025 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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.kosmos.collectValues
-import com.android.systemui.kosmos.runTest
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class DozingToDreamingTransitionViewModelTest : SysuiTestCase() {
-    val kosmos = testKosmos()
-
-    val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel }
-
-    @Test
-    fun notificationShadeAlpha() =
-        kosmos.runTest {
-            val values by collectValues(underTest.notificationAlpha)
-            assertThat(values).isEmpty()
-
-            fakeKeyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.DOZING,
-                to = KeyguardState.DREAMING,
-                testScope,
-            )
-
-            assertThat(values).isNotEmpty()
-            values.forEach { assertThat(it).isEqualTo(0) }
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index 25c1572..0b34a01 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.biometrics.authController
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -47,6 +48,7 @@
 import com.android.systemui.shade.domain.interactor.enableDualShade
 import com.android.systemui.shade.domain.interactor.enableSingleShade
 import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
 import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
@@ -123,6 +125,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun notificationsPlacement_dualShadeSmallClock_below() =
         kosmos.runTest {
             setupState(
@@ -135,6 +138,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun notificationsPlacement_dualShadeLargeClock_topStart() =
         kosmos.runTest {
             setupState(
@@ -156,6 +160,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun areNotificationsVisible_dualShadeWideOnLockscreen_true() =
         kosmos.runTest {
             setupState(
@@ -298,6 +303,7 @@
     ) {
         val isShadeLayoutWide by collectLastValue(kosmos.shadeRepository.isShadeLayoutWide)
         val collectedClockSize by collectLastValue(kosmos.keyguardClockInteractor.clockSize)
+        val collectedShadeMode by collectLastValue(kosmos.shadeModeInteractor.shadeMode)
         when (shadeMode) {
             ShadeMode.Dual -> kosmos.enableDualShade(wideLayout = shadeLayoutWide)
             ShadeMode.Single -> kosmos.enableSingleShade()
@@ -309,6 +315,7 @@
         if (shadeLayoutWide != null) {
             assertThat(isShadeLayoutWide).isEqualTo(shadeLayoutWide)
         }
+        assertThat(collectedShadeMode).isEqualTo(shadeMode)
         assertThat(collectedClockSize).isEqualTo(clockSize)
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index f5a7111..a7a0c24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -33,8 +33,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.WallpaperColors;
-import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
@@ -68,7 +66,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class MediaOutputAdapterTest extends SysuiTestCase {
+public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
 
     private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
     private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
@@ -92,8 +90,8 @@
 
     @Captor
     private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor;
-    private MediaOutputAdapter mMediaOutputAdapter;
-    private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
+    private MediaOutputAdapterLegacy mMediaOutputAdapter;
+    private MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy mViewHolder;
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
     private List<MediaItem> mMediaItems = new ArrayList<>();
     MediaOutputSeekbar mSpyMediaOutputSeekbar;
@@ -124,9 +122,9 @@
         mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
         mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));
 
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
     }
@@ -150,9 +148,9 @@
 
     @Test
     public void onBindViewHolder_bindPairNew_verifyView() {
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
         mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
@@ -175,9 +173,9 @@
                                 .map((item) -> item.getMediaDevice().get())
                                 .collect(Collectors.toList()));
         when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.getItemCount();
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -197,9 +195,9 @@
                                 .map((item) -> item.getMediaDevice().get())
                                 .collect(Collectors.toList()));
         when(mMediaSwitchingController.getSessionName()).thenReturn(null);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.getItemCount();
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -225,7 +223,7 @@
     @Test
     public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() {
         when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -245,7 +243,7 @@
         when(mMediaSwitchingController.getSelectableMediaDevice())
                 .thenReturn(ImmutableList.of(mMediaDevice2));
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -264,7 +262,7 @@
         when(mMediaSwitchingController.getSelectableMediaDevice())
                 .thenReturn(ImmutableList.of(mMediaDevice2));
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -276,7 +274,7 @@
     public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() {
         when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -294,7 +292,7 @@
         when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
         when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -315,7 +313,7 @@
         when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
         when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -348,7 +346,7 @@
         when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
         when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -498,7 +496,7 @@
         when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
         when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
         when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -522,7 +520,7 @@
         when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED);
         when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
@@ -545,7 +543,7 @@
         when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED);
         when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_NONE);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
@@ -567,7 +565,7 @@
         when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
         when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
         when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -627,9 +625,9 @@
 
     @Test
     public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
         mMediaOutputAdapter.updateItems();
@@ -645,9 +643,9 @@
         assertThat(mMediaDevice2.getState()).isEqualTo(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.getItemCount();
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -663,11 +661,12 @@
         assertThat(mMediaDevice2.getState()).isEqualTo(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
-        MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder);
+        MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy(
+                mViewHolder);
         mMediaOutputAdapter.getItemCount();
 
         mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0);
@@ -684,11 +683,12 @@
         when(mMediaDevice2.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
-        MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder);
+        MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy(
+                mViewHolder);
 
         mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1);
         spyMediaDeviceViewHolder.mContainerLayout.performClick();
@@ -715,7 +715,7 @@
         List<MediaDevice> selectableDevices = new ArrayList<>();
         selectableDevices.add(mMediaDevice2);
         when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
@@ -859,7 +859,7 @@
         when(mMediaSwitchingController.getSelectedMediaDevice())
                 .thenReturn(ImmutableList.of(mMediaDevice1));
         when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
@@ -899,16 +899,6 @@
     }
 
     @Test
-    public void updateColorScheme_triggerController() {
-        WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(
-                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888));
-
-        mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
-
-        verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true);
-    }
-
-    @Test
     public void updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() {
         mMediaOutputAdapter.updateItems();
         List<MediaItem> updatedList = new ArrayList<>();
@@ -990,7 +980,7 @@
     public void multipleSelectedDevices_verifySessionView() {
         initializeSession();
 
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -1011,7 +1001,7 @@
     public void multipleSelectedDevices_verifyCollapsedView() {
         initializeSession();
 
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -1024,13 +1014,13 @@
     @Test
     public void multipleSelectedDevices_expandIconClicked_verifyInitialView() {
         initializeSession();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         mViewHolder.mEndTouchArea.performClick();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -1047,13 +1037,13 @@
     @Test
     public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() {
         initializeSession();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         mViewHolder.mEndTouchArea.performClick();
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -1075,7 +1065,7 @@
         when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices);
         when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>());
 
-        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+        mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
                 .onCreateViewHolder(
                         new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 0bba8bb..b23cd5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.notifications.ui.viewmodel
 
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -28,6 +29,8 @@
 import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -39,10 +42,13 @@
 import com.android.systemui.shade.domain.interactor.enableDualShade
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
+import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -50,6 +56,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
@@ -155,6 +162,36 @@
             assertThat(underTest.showClock).isFalse()
         }
 
+    @Test
+    fun showMedia_activeMedia_true() =
+        testScope.runTest {
+            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
+            runCurrent()
+
+            assertThat(underTest.showMedia).isTrue()
+        }
+
+    @Test
+    fun showMedia_noActiveMedia_false() =
+        testScope.runTest {
+            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false))
+            runCurrent()
+
+            assertThat(underTest.showMedia).isFalse()
+        }
+
+    @Test
+    fun showMedia_qsDisabled_false() =
+        testScope.runTest {
+            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
+            kosmos.fakeDisableFlagsRepository.disableFlags.update {
+                it.copy(disable2 = DISABLE2_QUICK_SETTINGS)
+            }
+            runCurrent()
+
+            assertThat(underTest.showMedia).isFalse()
+        }
+
     private fun TestScope.lockDevice() {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
         kosmos.powerInteractor.setAsleepForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
index c775bfd..9e400a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
@@ -34,6 +35,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
 class GridLayoutTypeInteractorTest : SysuiTestCase() {
     val kosmos = testKosmos()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
index 2e7aeb4..9fe783b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.panels.data.repository.QSColumnsRepository
@@ -76,6 +77,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun withDualShade_returnsCorrectValue() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
index fdbf42c..d5e502e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
@@ -36,6 +37,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -43,6 +45,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() {
@@ -63,6 +66,7 @@
     }
 
     @Test
+    @EnableSceneContainer
     fun shouldMediaShowInRow() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
index 241cdbf..4912c31 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.testCase
@@ -88,6 +89,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationNull_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
@@ -111,6 +113,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationQS_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
@@ -133,6 +136,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 80c7026..23a0f62 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -757,4 +757,46 @@
 
             verify(processor, never()).onSceneAboutToChange(any(), any())
         }
+
+    @Test
+    fun changeScene_sameScene_withFreeze() =
+        kosmos.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+            underTest.registerSceneStateProcessor(processor)
+            verify(processor, never()).onSceneAboutToChange(any(), any())
+            assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+
+            underTest.changeScene(
+                toScene = Scenes.Lockscreen,
+                loggingReason = "test",
+                sceneState = KeyguardState.AOD,
+                forceSettleToTargetScene = true,
+            )
+
+            verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+            assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(1)
+        }
+
+    @Test
+    fun changeScene_sameScene_withoutFreeze() =
+        kosmos.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+            underTest.registerSceneStateProcessor(processor)
+            verify(processor, never()).onSceneAboutToChange(any(), any())
+            assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+
+            underTest.changeScene(
+                toScene = Scenes.Lockscreen,
+                loggingReason = "test",
+                sceneState = KeyguardState.AOD,
+                forceSettleToTargetScene = false,
+            )
+
+            verify(processor, never()).onSceneAboutToChange(any(), any())
+            assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
index 668f568..d26e195 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
@@ -31,6 +32,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
 class ShadeModeInteractorImplTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
@@ -80,7 +82,7 @@
         }
 
     @Test
-    fun isDualShade_settingEnabled_returnsTrue() =
+    fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() =
         testScope.runTest {
             // TODO(b/391578667): Add a test case for user switching once the bug is fixed.
             val shadeMode by collectLastValue(underTest.shadeMode)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index b8f66ac..dde8678 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
@@ -59,6 +60,7 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -103,6 +105,7 @@
         }
 
     @Test
+    @EnableSceneContainer
     fun hydrateShadeMode_dualShadeEnabled() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 93ba8e1..064fd48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -28,6 +28,7 @@
 import android.content.pm.ApplicationInfo;
 import android.util.Log;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -52,6 +53,7 @@
 import java.util.function.Supplier;
 
 @SmallTest
+@FlakyTest(bugId = 395832204)
 @RunWith(AndroidJUnit4.class)
 public class PluginInstanceTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index cabe4af..5d19506 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -183,7 +184,7 @@
                         statusBarChipIcon = null,
                         promotedContent = PROMOTED_CONTENT,
                     ),
-                    32L,
+                    creationTime = 32L,
                 )
 
             val latest by collectLastValue(underTest.notificationChip)
@@ -246,7 +247,7 @@
                     statusBarChipIcon = mock(),
                     promotedContent = PROMOTED_CONTENT,
                 )
-            val underTest = factory.create(startingNotif, 123L)
+            val underTest = factory.create(startingNotif, creationTime = 123L)
             val latest by collectLastValue(underTest.notificationChip)
             assertThat(latest).isNotNull()
 
@@ -306,9 +307,10 @@
         }
 
     @Test
-    fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() =
+    fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() =
         kosmos.runTest {
             activityManagerRepository.fake.startingIsAppVisibleValue = true
+            fakeSystemClock.setCurrentTimeMillis(9000)
 
             val underTest =
                 factory.create(
@@ -325,12 +327,14 @@
 
             assertThat(latest).isNotNull()
             assertThat(latest!!.isAppVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
         }
 
     @Test
-    fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() =
+    fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() =
         kosmos.runTest {
             activityManagerRepository.fake.startingIsAppVisibleValue = false
+            fakeSystemClock.setCurrentTimeMillis(9000)
 
             val underTest =
                 factory.create(
@@ -347,11 +351,15 @@
 
             assertThat(latest).isNotNull()
             assertThat(latest!!.isAppVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isNull()
         }
 
     @Test
     fun notificationChip_updatesWhenAppIsVisible() =
         kosmos.runTest {
+            activityManagerRepository.fake.startingIsAppVisibleValue = false
+            fakeSystemClock.setCurrentTimeMillis(9000)
+
             val underTest =
                 factory.create(
                     activeNotificationModel(
@@ -365,32 +373,39 @@
 
             val latest by collectLastValue(underTest.notificationChip)
 
-            activityManagerRepository.fake.setIsAppVisible(UID, false)
+            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
             assertThat(latest!!.isAppVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isNull()
 
-            activityManagerRepository.fake.setIsAppVisible(UID, true)
+            fakeSystemClock.setCurrentTimeMillis(11000)
+            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
             assertThat(latest!!.isAppVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)
 
-            activityManagerRepository.fake.setIsAppVisible(UID, false)
+            fakeSystemClock.setCurrentTimeMillis(13000)
+            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
             assertThat(latest!!.isAppVisible).isFalse()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)
+
+            fakeSystemClock.setCurrentTimeMillis(15000)
+            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
+            assertThat(latest!!.isAppVisible).isTrue()
+            assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000)
         }
 
-    // Note: This test is theoretically impossible because the notification key should contain the
-    // UID, so if the UID changes then the key would also change and a new interactor would be
-    // created. But, test it just in case.
     @Test
-    fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() =
+    fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() =
         kosmos.runTest {
             activityManagerRepository.fake.startingIsAppVisibleValue = false
 
-            val hiddenUid = 100
-            val shownUid = 101
+            val originalUid = 100
+            val newUid = 101
 
             val underTest =
                 factory.create(
                     activeNotificationModel(
                         key = "notif",
-                        uid = hiddenUid,
+                        uid = originalUid,
                         statusBarChipIcon = mock(),
                         promotedContent = PROMOTED_CONTENT,
                     ),
@@ -402,16 +417,34 @@
 
             // WHEN the notif gets a new UID that starts as visible
             activityManagerRepository.fake.startingIsAppVisibleValue = true
+            val newPromotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.shortCriticalText = "Arrived"
+                }
+            val newPromotedContent = newPromotedContentBuilder.build()
             underTest.setNotification(
                 activeNotificationModel(
                     key = "notif",
-                    uid = shownUid,
+                    uid = newUid,
                     statusBarChipIcon = mock(),
-                    promotedContent = PROMOTED_CONTENT,
+                    promotedContent = newPromotedContent,
                 )
             )
 
-            // THEN we re-fetch the app visibility state with the new UID
+            // THEN we do update other fields like promoted content
+            assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent)
+
+            // THEN we don't fetch the app visibility state for the new UID
+            assertThat(latest!!.isAppVisible).isFalse()
+
+            // AND don't listen to updates for the new UID
+            activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false)
+            activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true)
+            assertThat(latest!!.isAppVisible).isFalse()
+
+            // AND we still use updates from the old UID
+            // TODO(b/364653005): This particular behavior isn't great, can we do better?
+            activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true)
             assertThat(latest!!.isAppVisible).isTrue()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index d8e4cd9..7ed2bd3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -333,7 +333,7 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun shownNotificationChips_sortedBasedOnFirstAppearanceTime() =
+    fun shownNotificationChips_sortedByFirstAppearanceTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.shownNotificationChips)
 
@@ -349,8 +349,7 @@
                     promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
                 )
             setNotifs(listOf(notif1))
-            assertThat(latest).hasSize(1)
-            assertThat(latest!![0].key).isEqualTo("notif1")
+            assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
 
             // WHEN we add notif2 at t=2000
             fakeSystemClock.advanceTime(1000)
@@ -362,26 +361,20 @@
                 )
             setNotifs(listOf(notif1, notif2))
 
-            // THEN notif2 is ranked above notif1 because it appeared later
-            assertThat(latest).hasSize(2)
-            assertThat(latest!![0].key).isEqualTo("notif2")
-            assertThat(latest!![1].key).isEqualTo("notif1")
+            // THEN notif2 is ranked above notif1 because notif2 appeared later
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
 
             // WHEN notif1 and notif2 swap places
             setNotifs(listOf(notif2, notif1))
 
             // THEN notif2 is still ranked above notif1 to preserve chip ordering
-            assertThat(latest).hasSize(2)
-            assertThat(latest!![0].key).isEqualTo("notif2")
-            assertThat(latest!![1].key).isEqualTo("notif1")
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
 
             // WHEN notif1 and notif2 swap places again
             setNotifs(listOf(notif1, notif2))
 
             // THEN notif2 is still ranked above notif1 to preserve chip ordering
-            assertThat(latest).hasSize(2)
-            assertThat(latest!![0].key).isEqualTo("notif2")
-            assertThat(latest!![1].key).isEqualTo("notif1")
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
 
             // WHEN notif1 gets an update
             val notif1NewPromotedContent =
@@ -400,9 +393,7 @@
             )
 
             // THEN notif2 is still ranked above notif1 to preserve chip ordering
-            assertThat(latest).hasSize(2)
-            assertThat(latest!![0].key).isEqualTo("notif2")
-            assertThat(latest!![1].key).isEqualTo("notif1")
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
 
             // WHEN notif1 disappears and then reappears
             fakeSystemClock.advanceTime(1000)
@@ -413,9 +404,238 @@
             setNotifs(listOf(notif2, notif1))
 
             // THEN notif1 is now ranked first
-            assertThat(latest).hasSize(2)
-            assertThat(latest!![0].key).isEqualTo("notif1")
-            assertThat(latest!![1].key).isEqualTo("notif2")
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+        }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun shownNotificationChips_sortedByLastAppVisibleTime() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.shownNotificationChips)
+
+            val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+            val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+
+            activityManagerRepository.fake.startingIsAppVisibleValue = false
+            fakeSystemClock.setCurrentTimeMillis(1000)
+            val notif1 =
+                activeNotificationModel(
+                    key = notif1Info.key,
+                    uid = notif1Info.uid,
+                    statusBarChipIcon = notif1Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                )
+            val notif2 =
+                activeNotificationModel(
+                    key = notif2Info.key,
+                    uid = notif2Info.uid,
+                    statusBarChipIcon = notif2Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                )
+            setNotifs(listOf(notif1, notif2))
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+            // WHEN notif2's app becomes visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+
+            // THEN notif2 is no longer shown
+            assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
+
+            // WHEN notif2's app is no longer visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+            // THEN notif2 is ranked above notif1 because it was more recently visible
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+            // WHEN the app associated with notif1 becomes visible then un-visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true)
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
+
+            // THEN notif1 is now ranked above notif2 because it was more recently visible
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+        }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun shownNotificationChips_newNotificationTakesPriorityOverLastAppVisible() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.shownNotificationChips)
+
+            val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+            val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+            val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300)
+
+            activityManagerRepository.fake.startingIsAppVisibleValue = false
+            fakeSystemClock.setCurrentTimeMillis(1000)
+            val notif1 =
+                activeNotificationModel(
+                    key = notif1Info.key,
+                    uid = notif1Info.uid,
+                    statusBarChipIcon = notif1Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                )
+            val notif2 =
+                activeNotificationModel(
+                    key = notif2Info.key,
+                    uid = notif2Info.uid,
+                    statusBarChipIcon = notif2Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                )
+            setNotifs(listOf(notif1, notif2))
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+            // WHEN notif2's app becomes visible then not visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+            // THEN notif2 is ranked above notif1 because it was more recently visible
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+            // WHEN a new notif3 appears
+            fakeSystemClock.advanceTime(1000)
+            val notif3 =
+                activeNotificationModel(
+                    key = notif3Info.key,
+                    uid = notif3Info.uid,
+                    statusBarChipIcon = notif3Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                )
+            setNotifs(listOf(notif1, notif2, notif3))
+
+            // THEN notif3 is ranked above everything else
+            // AND notif2 is still before notif1 because it was more recently visible
+            assertThat(latest!!.map { it.key })
+                .containsExactly("notif3", "notif2", "notif1")
+                .inOrder()
+        }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun shownNotificationChips_fullSort() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.shownNotificationChips)
+
+            val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+            val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+            val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300)
+
+            // First, add notif1 at t=1000
+            activityManagerRepository.fake.startingIsAppVisibleValue = false
+            fakeSystemClock.setCurrentTimeMillis(1000)
+            val notif1 =
+                activeNotificationModel(
+                    key = notif1Info.key,
+                    uid = notif1Info.uid,
+                    statusBarChipIcon = notif1Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                )
+            setNotifs(listOf(notif1))
+
+            // WHEN we add notif2 at t=2000
+            fakeSystemClock.advanceTime(1000)
+            val notif2 =
+                activeNotificationModel(
+                    key = notif2Info.key,
+                    uid = notif2Info.uid,
+                    statusBarChipIcon = notif2Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                )
+            setNotifs(listOf(notif1, notif2))
+
+            // THEN notif2 is ranked above notif1 because notif2 appeared later
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+            // WHEN notif2's app becomes visible then un-visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+            // THEN notif2 is ranked above notif1 because it was more recently visible
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+            // WHEN the app associated with notif1 becomes visible then un-visible
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true)
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
+
+            // THEN notif1 is ranked above notif2 because it was more recently visible
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+            // WHEN notif2 gets an update
+            val notif2NewPromotedContent =
+                PromotedNotificationContentModel.Builder("notif2").apply {
+                    this.shortCriticalText = "Arrived"
+                }
+            setNotifs(
+                listOf(
+                    notif1,
+                    activeNotificationModel(
+                        key = notif2Info.key,
+                        uid = notif2Info.uid,
+                        statusBarChipIcon = notif2Info.icon,
+                        promotedContent = notif2NewPromotedContent.build(),
+                    ),
+                )
+            )
+
+            // THEN notif1 is still ranked above notif2 to preserve chip ordering
+            assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+            // WHEN a new notification appears
+            fakeSystemClock.advanceTime(1000)
+            val notif3 =
+                activeNotificationModel(
+                    key = notif3Info.key,
+                    uid = notif3Info.uid,
+                    statusBarChipIcon = notif3Info.icon,
+                    promotedContent =
+                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                )
+            setNotifs(listOf(notif1, notif2, notif3))
+
+            // THEN it's ranked first because it's new
+            assertThat(latest!!.map { it.key })
+                .containsExactly("notif3", "notif1", "notif2")
+                .inOrder()
+
+            // WHEN notif2 becomes visible then un-visible again
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+            fakeSystemClock.advanceTime(1000)
+            activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+            // THEN it moves to the front
+            assertThat(latest!!.map { it.key })
+                .containsExactly("notif2", "notif3", "notif1")
+                .inOrder()
+
+            // WHEN notif1 disappears and then reappears
+            fakeSystemClock.advanceTime(1000)
+            setNotifs(listOf(notif2, notif3))
+            assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif3").inOrder()
+
+            fakeSystemClock.advanceTime(1000)
+            setNotifs(listOf(notif2, notif1, notif3))
+
+            // THEN notif1 is now ranked first
+            assertThat(latest!!.map { it.key })
+                .containsExactly("notif1", "notif2", "notif3")
+                .inOrder()
         }
 
     @Test
@@ -495,4 +715,6 @@
                 .apply { notifs.forEach { addIndividualNotif(it) } }
                 .build()
     }
+
+    private data class NotifInfo(val key: String, val icon: StatusBarIconView, val uid: Int)
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index aaa9b58..7cf817a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -49,8 +49,11 @@
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Before
 import org.junit.runner.RunWith
@@ -286,13 +289,15 @@
     fun chips_hasShortCriticalText_usesTextInsteadOfTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.shortCriticalText = "Arrived"
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 30.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -340,13 +345,15 @@
     fun chips_basicTime_timeHiddenIfAutomaticallyPromoted() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.wasPromotedAutomatically = true
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 30.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -370,13 +377,15 @@
     fun chips_basicTime_timeShownIfNotAutomaticallyPromoted() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.wasPromotedAutomatically = false
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 30.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -397,18 +406,21 @@
 
     @Test
     @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
-    fun chips_basicTime_isShortTimeDelta() =
+    fun chips_basicTime_timeInFuture_isShortTimeDelta() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 3.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 13.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
+
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -426,15 +438,152 @@
 
     @Test
     @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+            val currentTime = 3.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = currentTime + 500,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_basicTime_timeIsNow_isIconOnly() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+            val currentTime = 62.seconds.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = currentTime,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_basicTime_timeInPast_isIconOnly() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+            val currentTime = 62.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = currentTime - 2.minutes.inWholeMilliseconds,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+        }
+
+    // Not necessarily the behavior we *want* to have, but it's the currently implemented behavior.
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_basicTime_timeIsInFuture_thenTimeAdvances_stillShortTimeDelta() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = currentTime + 3.minutes.inWholeMilliseconds,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java)
+
+            fakeSystemClock.advanceTime(5.minutes.inWholeMilliseconds)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java)
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
     fun chips_countUpTime_isTimer() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.CountUp,
                         )
                 }
@@ -457,12 +606,14 @@
     fun chips_countDownTime_isTimer() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.CountDown,
                         )
                 }
@@ -485,12 +636,14 @@
     fun chips_noHeadsUp_showsTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -517,12 +670,14 @@
     fun chips_hasHeadsUpBySystem_showsTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -556,12 +711,14 @@
     fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -569,7 +726,7 @@
                 PromotedNotificationContentModel.Builder("other notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 654321L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
@@ -610,12 +767,14 @@
     fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+            val currentTime = 30.minutes.inWholeMilliseconds
+            fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
                 PromotedNotificationContentModel.Builder("notif").apply {
                     this.time =
                         PromotedNotificationContentModel.When(
-                            time = 6543L,
+                            time = currentTime + 10.minutes.inWholeMilliseconds,
                             mode = PromotedNotificationContentModel.When.Mode.BasicTime,
                         )
                 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
index 1a5f57d..6409a20 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
@@ -17,100 +17,119 @@
 package com.android.systemui.statusbar.featurepods.media.domain.interactor
 
 import android.graphics.drawable.Drawable
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaAction
 import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
+@RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class MediaControlChipInteractorTest : SysuiTestCase() {
-
+class MediaControlChipInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
-    private val underTest = kosmos.mediaControlChipInteractor
+    private val mediaFilterRepository = kosmos.mediaFilterRepository
+    private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipInteractor }
+    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return parameterizeSceneContainerFlag()
+        }
+    }
+
+    @Before
+    fun setUp() {
+        kosmos.underTest.initialize()
+        MockitoAnnotations.initMocks(this)
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Test
-    fun mediaControlModel_noActiveMedia_null() =
+    fun mediaControlChipModel_noActiveMedia_null() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             assertThat(model).isNull()
         }
 
     @Test
-    fun mediaControlModel_activeMedia_notNull() =
+    fun mediaControlChipModel_activeMedia_notNull() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             val userMedia = MediaData(active = true)
-            val instanceId = userMedia.instanceId
 
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(model).isNotNull()
         }
 
     @Test
-    fun mediaControlModel_mediaRemoved_null() =
+    fun mediaControlChipModel_mediaRemoved_null() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             val userMedia = MediaData(active = true)
-            val instanceId = userMedia.instanceId
 
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(model).isNotNull()
 
-            assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia))
-                .isTrue()
-            mediaFilterRepository.addMediaDataLoadingState(
-                MediaDataLoadingModel.Removed(instanceId)
-            )
+            removeMedia(userMedia)
 
             assertThat(model).isNull()
         }
 
     @Test
-    fun mediaControlModel_songNameChanged_emitsUpdatedModel() =
+    fun mediaControlChipModel_songNameChanged_emitsUpdatedModel() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             val initialSongName = "Initial Song"
             val newSongName = "New Song"
             val userMedia = MediaData(active = true, song = initialSongName)
-            val instanceId = userMedia.instanceId
 
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(model).isNotNull()
             assertThat(model?.songName).isEqualTo(initialSongName)
 
             val updatedUserMedia = userMedia.copy(song = newSongName)
-            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+            updateMedia(updatedUserMedia)
 
             assertThat(model?.songName).isEqualTo(newSongName)
         }
 
     @Test
-    fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() =
+    fun mediaControlChipModel_playPauseActionChanges_emitsUpdatedModel() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             val mockDrawable = mock<Drawable>()
 
@@ -123,9 +142,7 @@
                 )
             val mediaButton = MediaButton(playOrPause = initialAction)
             val userMedia = MediaData(active = true, semanticActions = mediaButton)
-            val instanceId = userMedia.instanceId
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(model).isNotNull()
             assertThat(model?.playOrPause).isEqualTo(initialAction)
@@ -139,15 +156,15 @@
                 )
             val updatedMediaButton = MediaButton(playOrPause = newAction)
             val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton)
-            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+            updateMedia(updatedUserMedia)
 
             assertThat(model?.playOrPause).isEqualTo(newAction)
         }
 
     @Test
-    fun mediaControlModel_playPauseActionRemoved_playPauseNull() =
+    fun mediaControlChipModel_playPauseActionRemoved_playPauseNull() =
         kosmos.runTest {
-            val model by collectLastValue(underTest.mediaControlModel)
+            val model by collectLastValue(underTest.mediaControlChipModel)
 
             val mockDrawable = mock<Drawable>()
 
@@ -160,16 +177,36 @@
                 )
             val mediaButton = MediaButton(playOrPause = initialAction)
             val userMedia = MediaData(active = true, semanticActions = mediaButton)
-            val instanceId = userMedia.instanceId
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(model).isNotNull()
             assertThat(model?.playOrPause).isEqualTo(initialAction)
 
             val updatedUserMedia = userMedia.copy(semanticActions = MediaButton())
-            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+            updateMedia(updatedUserMedia)
 
             assertThat(model?.playOrPause).isNull()
         }
+
+    private fun updateMedia(mediaData: MediaData) {
+        if (SceneContainerFlag.isEnabled) {
+            val instanceId = mediaData.instanceId
+            mediaFilterRepository.addSelectedUserMediaEntry(mediaData)
+            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+        } else {
+            kosmos.underTest.updateMediaControlChipModelLegacy(mediaData)
+        }
+    }
+
+    private fun removeMedia(mediaData: MediaData) {
+        if (SceneContainerFlag.isEnabled) {
+            val instanceId = mediaData.instanceId
+            mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, mediaData)
+            mediaFilterRepository.addMediaDataLoadingState(
+                MediaDataLoadingModel.Removed(instanceId)
+            )
+        } else {
+            kosmos.underTest.updateMediaControlChipModelLegacy(null)
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
index 8650e4b..d36dbbe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
@@ -16,26 +16,57 @@
 
 package com.android.systemui.statusbar.featurepods.media.ui.viewmodel
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import org.junit.Before
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class MediaControlChipViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
-    private val underTest = kosmos.mediaControlChipViewModel
+    private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor }
+    private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel }
+    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return parameterizeSceneContainerFlag()
+        }
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mediaControlChipInteractor.initialize()
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Test
     fun chip_noActiveMedia_IsHidden() =
@@ -51,10 +82,7 @@
             val chip by collectLastValue(underTest.chip)
 
             val userMedia = MediaData(active = true, song = "test")
-            val instanceId = userMedia.instanceId
-
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+            updateMedia(userMedia)
 
             assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
         }
@@ -67,16 +95,25 @@
             val initialSongName = "Initial Song"
             val newSongName = "New Song"
             val userMedia = MediaData(active = true, song = initialSongName)
-            val instanceId = userMedia.instanceId
-
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
-
+            updateMedia(userMedia)
+            assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
             assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName)
 
             val updatedUserMedia = userMedia.copy(song = newSongName)
-            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+            updateMedia(updatedUserMedia)
 
             assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName)
         }
+
+    private fun updateMedia(mediaData: MediaData) {
+        if (SceneContainerFlag.isEnabled) {
+            val instanceId = mediaData.instanceId
+            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(mediaData)
+            kosmos.mediaFilterRepository.addMediaDataLoadingState(
+                MediaDataLoadingModel.Loaded(instanceId)
+            )
+        } else {
+            mediaControlChipInteractor.updateMediaControlChipModelLegacy(mediaData)
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
deleted file mode 100644
index fcbf0fe..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2024 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.featurepods.popups.ui.viewmodel
-
-import android.platform.test.annotations.EnableFlags
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.data.repository.mediaFilterRepository
-import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
-import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@EnableFlags(StatusBarPopupChips.FLAG_NAME)
-@RunWith(AndroidJUnit4::class)
-class StatusBarPopupChipsViewModelTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-    private val mediaFilterRepository = kosmos.mediaFilterRepository
-    private val underTest = kosmos.statusBarPopupChipsViewModel
-
-    @Test
-    fun shownPopupChips_allHidden_empty() =
-        testScope.runTest {
-            val shownPopupChips by collectLastValue(underTest.shownPopupChips)
-            assertThat(shownPopupChips).isEmpty()
-        }
-
-    @Test
-    fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() =
-        testScope.runTest {
-            val shownPopupChips by collectLastValue(underTest.shownPopupChips)
-
-            val userMedia = MediaData(active = true, song = "test")
-            val instanceId = userMedia.instanceId
-
-            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
-
-            assertThat(shownPopupChips).hasSize(1)
-            assertThat(shownPopupChips!!.first().chipId).isEqualTo(PopupChipId.MediaControl)
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
index d66b010..a58f7f7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
@@ -51,8 +51,10 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.people.NotificationPersonExtractor;
 import com.android.systemui.util.DeviceConfigProxyFake;
 
+import org.jetbrains.annotations.NotNull;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
index 77fd067..8520508 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
@@ -132,7 +132,7 @@
         LayoutInflater inflater = LayoutInflater.from(mContext);
         inflater.setFactory2(
                 new RowInflaterTask.RowAsyncLayoutInflater(entry, new FakeSystemClock(), mock(
-                        RowInflaterTaskLogger.class)));
+                        RowInflaterTaskLogger.class), mContext.getUser()));
 
         ExpandableNotificationRow row = (ExpandableNotificationRow)
                 inflater.inflate(R.layout.status_bar_notification_row, null);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
new file mode 100644
index 0000000..426af26
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 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.notification.collection
+
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class BundleEntryTest : SysuiTestCase() {
+    private lateinit var underTest: BundleEntry
+
+    @get:Rule
+    val setFlagsRule = SetFlagsRule()
+
+    @Before
+    fun setUp() {
+        underTest = BundleEntry("key")
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getParent_adapter() {
+        assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY)
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isTopLevelEntry_adapter() {
+        assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getRow_adapter() {
+        assertThat(underTest.entryAdapter.row).isNull()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getGroupRoot_adapter() {
+        assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter)
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getKey_adapter() {
+        assertThat(underTest.entryAdapter.key).isEqualTo("key")
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
index 8e95ac5..76e2d61 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
@@ -30,6 +30,9 @@
 
 import android.app.Notification;
 import android.app.NotificationChannel;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -39,8 +42,10 @@
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -57,6 +62,9 @@
     @Mock private GroupMembershipManager mGroupMembershipManager;
     private HighPriorityProvider mHighPriorityProvider;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -210,6 +218,7 @@
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     public void testIsHighPriority_checkChildrenToCalculatePriority_legacy() {
         // GIVEN: a summary with low priority has a highPriorityChild and a lowPriorityChild
         final NotificationEntry summary = createNotifEntry(false);
@@ -247,20 +256,18 @@
     }
 
     @Test
-    public void testIsHighPriority_checkChildrenToCalculatePriority() {
+    public void testIsHighPriority_checkChildrenViewsToCalculatePriority() {
         // GIVEN:
         // parent with summary = lowPrioritySummary
         //      NotificationEntry = lowPriorityChild
         //      NotificationEntry = highPriorityChild
+        List<NotificationEntry> children = List.of(createNotifEntry(false), createNotifEntry(true));
         final NotificationEntry lowPrioritySummary = createNotifEntry(false);
         final GroupEntry parentEntry = new GroupEntryBuilder()
                 .setSummary(lowPrioritySummary)
+                .setChildren(children)
                 .build();
-        when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(
-                new ArrayList<>(
-                        List.of(
-                                createNotifEntry(false),
-                                createNotifEntry(true))));
+        when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(children);
 
         // THEN the GroupEntry parentEntry is high priority since it has a high priority child
         assertTrue(mHighPriorityProvider.isHighPriority(parentEntry));
@@ -272,10 +279,11 @@
         // parent with summary = lowPrioritySummary
         //      NotificationEntry = lowPriorityChild
         final NotificationEntry lowPrioritySummary = createNotifEntry(false);
+        final NotificationEntry lowPriorityChild = createNotifEntry(false);
         final GroupEntry parentEntry = new GroupEntryBuilder()
                 .setSummary(lowPrioritySummary)
+                .setChildren(List.of(lowPriorityChild))
                 .build();
-        final NotificationEntry lowPriorityChild = createNotifEntry(false);
         when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(
                 new ArrayList<>(List.of(lowPriorityChild)));
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
index e93c742..7fa157f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
@@ -27,14 +27,29 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.buildNotificationEntry
+import com.android.systemui.statusbar.notification.buildOngoingCallEntry
+import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry
 import com.android.systemui.statusbar.notification.collection.buildEntry
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.notifPipeline
+import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -59,7 +74,13 @@
     fun setup() {
         allowTestableLooperAsMainThread()
 
-        colorizedFgsCoordinator = ColorizedFgsCoordinator()
+        kosmos.statusBarNotificationChipsInteractor.start()
+
+        colorizedFgsCoordinator =
+            ColorizedFgsCoordinator(
+                kosmos.applicationCoroutineScope,
+                kosmos.promotedNotificationsInteractor,
+            )
         colorizedFgsCoordinator.attach(notifPipeline)
         sectioner = colorizedFgsCoordinator.sectioner
     }
@@ -178,6 +199,37 @@
         verify(notifPipeline, never()).addPromoter(any())
     }
 
+    @Test
+    @EnableFlags(
+        PromotedNotificationUi.FLAG_NAME,
+        StatusBarNotifChips.FLAG_NAME,
+        StatusBarChipsModernization.FLAG_NAME,
+        StatusBarRootModernization.FLAG_NAME,
+    )
+    fun comparatorPutsCallBeforeOther() =
+        kosmos.runTest {
+            // GIVEN a call and a promoted ongoing notification
+            val callEntry = buildOngoingCallEntry(promoted = false)
+            val ronEntry = buildPromotedOngoingEntry()
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            kosmos.renderNotificationListInteractor.setRenderedList(
+                listOf(callEntry, ronEntry, otherEntry)
+            )
+
+            val orderedChipNotificationKeys by
+                collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys)
+
+            // THEN the order of the notification keys should be the call then the RON
+            assertThat(orderedChipNotificationKeys)
+                .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+
+            // VERIFY that the comparator puts the call before the ron
+            assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0)
+            // VERIFY that the comparator puts the ron before the other
+            assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0)
+        }
+
     private fun makeCallStyle(): Notification.CallStyle {
         val pendingIntent =
             PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index db5921d..3dd0982 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -17,23 +17,29 @@
 package com.android.systemui.statusbar.notification.collection.render
 
 import android.os.Build
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.assertLogsWtf
+import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assume
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.never
@@ -44,6 +50,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class GroupExpansionManagerTest : SysuiTestCase() {
+    @get:Rule
+    val setFlagsRule = SetFlagsRule()
+
     private lateinit var underTest: GroupExpansionManagerImpl
 
     private val dumpManager: DumpManager = mock()
@@ -52,8 +61,8 @@
     private val pipeline: NotifPipeline = mock()
     private lateinit var beforeRenderListListener: OnBeforeRenderListListener
 
-    private val summary1 = notificationEntry("foo", 1)
-    private val summary2 = notificationEntry("bar", 1)
+    private val summary1 = notificationSummaryEntry("foo", 1)
+    private val summary2 = notificationSummaryEntry("bar", 1)
     private val entries =
         listOf<ListEntry>(
             GroupEntryBuilder()
@@ -82,15 +91,25 @@
     private fun notificationEntry(pkg: String, id: Int) =
         NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() }
 
+    private fun notificationSummaryEntry(pkg: String, id: Int) =
+        NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build()
+            .apply { row = mock() }
+
     @Before
     fun setUp() {
         whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
         whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
 
+        whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter))
+            .thenReturn(summary1.entryAdapter)
+        whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter))
+            .thenReturn(summary2.entryAdapter)
+
         underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager)
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun notifyOnlyOnChange() {
         var listenerCalledCount = 0
         underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
@@ -108,6 +127,25 @@
     }
 
     @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun notifyOnlyOnChange_withEntryAdapter() {
+        var listenerCalledCount = 0
+        underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+        underTest.setGroupExpanded(summary1.entryAdapter, false)
+        assertThat(listenerCalledCount).isEqualTo(0)
+        underTest.setGroupExpanded(summary1.entryAdapter, true)
+        assertThat(listenerCalledCount).isEqualTo(1)
+        underTest.setGroupExpanded(summary2.entryAdapter, true)
+        assertThat(listenerCalledCount).isEqualTo(2)
+        underTest.setGroupExpanded(summary1.entryAdapter, true)
+        assertThat(listenerCalledCount).isEqualTo(2)
+        underTest.setGroupExpanded(summary2.entryAdapter, false)
+        assertThat(listenerCalledCount).isEqualTo(3)
+    }
+
+    @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun expandUnattachedEntry() {
         // First, expand the entry when it is attached.
         underTest.setGroupExpanded(summary1, true)
@@ -122,6 +160,22 @@
     }
 
     @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun expandUnattachedEntryAdapter() {
+        // First, expand the entry when it is attached.
+        underTest.setGroupExpanded(summary1.entryAdapter, true)
+        assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue()
+
+        // Un-attach it, and un-expand it.
+        NotificationEntryBuilder.setNewParent(summary1, null)
+        underTest.setGroupExpanded(summary1.entryAdapter, false)
+
+        // Expanding again should throw.
+        assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) }
+    }
+
+    @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun syncWithPipeline() {
         underTest.attach(pipeline)
         beforeRenderListListener = withArgCaptor {
@@ -143,4 +197,28 @@
         verify(listener).onGroupExpansionChange(summary1.row, false)
         verifyNoMoreInteractions(listener)
     }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun syncWithPipeline_withEntryAdapter() {
+        underTest.attach(pipeline)
+        beforeRenderListListener = withArgCaptor {
+            verify(pipeline).addOnBeforeRenderListListener(capture())
+        }
+
+        val listener: OnGroupExpansionChangeListener = mock()
+        underTest.registerGroupExpansionChangeListener(listener)
+
+        beforeRenderListListener.onBeforeRenderList(entries)
+        verify(listener, never()).onGroupExpansionChange(any(), any())
+
+        // Expand one of the groups.
+        underTest.setGroupExpanded(summary1.entryAdapter, true)
+        verify(listener).onGroupExpansionChange(summary1.row, true)
+
+        // Empty the pipeline list and verify that the group is no longer expanded.
+        beforeRenderListListener.onBeforeRenderList(emptyList())
+        verify(listener).onGroupExpansionChange(summary1.row, false)
+        verifyNoMoreInteractions(listener)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
index 2cbcc5a..dcbf44e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -16,34 +16,46 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class GroupMembershipManagerTest : SysuiTestCase() {
+
+    @get:Rule
+    val setFlagsRule = SetFlagsRule()
+
     private var underTest = GroupMembershipManagerImpl()
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isChildInGroup_topLevel() {
         val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
         assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse()
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isChildInGroup_noParent() {
         val noParentEntry = NotificationEntryBuilder().setParent(null).build()
         assertThat(underTest.isChildInGroup(noParentEntry)).isFalse()
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isChildInGroup_summary() {
         val groupKey = "group"
         val summary =
@@ -57,12 +69,14 @@
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isGroupSummary_topLevelEntry() {
         val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
         assertThat(underTest.isGroupSummary(entry)).isFalse()
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isGroupSummary_summary() {
         val groupKey = "group"
         val summary =
@@ -76,6 +90,7 @@
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun isGroupSummary_child() {
         val groupKey = "group"
         val summary =
@@ -90,12 +105,14 @@
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun getGroupSummary_topLevelEntry() {
         val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
         assertThat(underTest.getGroupSummary(entry)).isNull()
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun getGroupSummary_summary() {
         val groupKey = "group"
         val summary =
@@ -109,6 +126,7 @@
     }
 
     @Test
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun getGroupSummary_child() {
         val groupKey = "group"
         val summary =
@@ -121,4 +139,104 @@
 
         assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary)
     }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isChildEntryAdapterInGroup_topLevel() {
+        val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+        assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isChildEntryAdapterInGroup_noParent() {
+        val noParentEntry = NotificationEntryBuilder().setParent(null).build()
+        assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isChildEntryAdapterInGroup_summary() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isGroupRoot_topLevelEntry() {
+        val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+        assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isGroupRoot_summary() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isGroupRoot_child() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+        assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getGroupRoot_topLevelEntry() {
+        val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+        assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getGroupRoot_summary() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter)
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun getGroupRoot_child() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+        assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index 5ba972de..7cbc839 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -140,7 +140,7 @@
 
     @Test
     @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
-    @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @DisableFlags(Flags.FLAG_MODES_UI)
     fun text_changesWhenNotifsHiddenInShade() =
         testScope.runTest {
             val text by collectLastValue(underTest.text)
@@ -163,7 +163,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun text_changesWhenLocaleChanges() =
         testScope.runTest {
             val text by collectLastValue(underTest.text)
@@ -186,7 +186,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun text_reflectsModesHidingNotifications() =
         testScope.runTest {
             val text by collectLastValue(underTest.text)
@@ -250,7 +250,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
         testScope.runTest {
             val onClick by collectLastValue(underTest.onClick)
@@ -264,7 +264,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
         testScope.runTest {
             val onClick by collectLastValue(underTest.onClick)
@@ -279,7 +279,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
         testScope.runTest {
             val onClick by collectLastValue(underTest.onClick)
@@ -305,7 +305,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
         testScope.runTest {
             val onClick by collectLastValue(underTest.onClick)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 339f8fa..e22acd5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -106,11 +106,15 @@
             this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME)
             this.addOverride(
                 R.integer.heads_up_notification_minimum_time,
-                TEST_MINIMUM_DISPLAY_TIME,
+                TEST_MINIMUM_DISPLAY_TIME_DEFAULT,
             )
             this.addOverride(
                 R.integer.heads_up_notification_minimum_time_with_throttling,
-                TEST_MINIMUM_DISPLAY_TIME,
+                TEST_MINIMUM_DISPLAY_TIME_DEFAULT,
+            )
+            this.addOverride(
+                R.integer.heads_up_notification_minimum_time_for_user_initiated,
+                TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED,
             )
             this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME)
             this.addOverride(
@@ -414,7 +418,7 @@
     }
 
     @Test
-    fun testRemoveNotification_beforeMinimumDisplayTime() {
+    fun testRemoveNotification_beforeMinimumDisplayTime_notUserInitiatedHun() {
         val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
         useAccessibilityTimeout(false)
 
@@ -429,18 +433,22 @@
         assertThat(removedImmediately).isFalse()
         assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
 
-        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+        systemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
 
         assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
     }
 
     @Test
-    fun testRemoveNotification_afterMinimumDisplayTime() {
+    fun testRemoveNotification_afterMinimumDisplayTime_notUserInitiatedHun() {
         val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
         useAccessibilityTimeout(false)
 
         underTest.showNotification(entry)
-        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+        systemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
 
         assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
 
@@ -455,6 +463,57 @@
     }
 
     @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun testRemoveNotification_beforeMinimumDisplayTime_forUserInitiatedHun() {
+        useAccessibilityTimeout(false)
+
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        entry.row = testHelper.createRow()
+        underTest.showNotification(entry, isPinnedByUser = true)
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "beforeMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isFalse()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+        systemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun testRemoveNotification_afterMinimumDisplayTime_forUserInitiatedHun() {
+        useAccessibilityTimeout(false)
+
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        entry.row = testHelper.createRow()
+        underTest.showNotification(entry, isPinnedByUser = true)
+
+        systemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "afterMinimumDisplayTime",
+            )
+
+        assertThat(removedImmediately).isTrue()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
     fun testRemoveNotification_releaseImmediately() {
         val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
 
@@ -1047,16 +1106,21 @@
     }
 
     companion object {
-        const val TEST_TOUCH_ACCEPTANCE_TIME = 200
-        const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
-        const val TEST_EXTENSION_TIME = 500
+        private const val TEST_TOUCH_ACCEPTANCE_TIME = 200
+        private const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
+        private const val TEST_EXTENSION_TIME = 500
 
-        const val TEST_MINIMUM_DISPLAY_TIME = 400
-        const val TEST_AUTO_DISMISS_TIME = 600
-        const val TEST_STICKY_AUTO_DISMISS_TIME = 800
+        private const val TEST_MINIMUM_DISPLAY_TIME_DEFAULT = 400
+        private const val TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED = 500
+        private const val TEST_AUTO_DISMISS_TIME = 600
+        private const val TEST_STICKY_AUTO_DISMISS_TIME = 800
 
         init {
-            assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+            assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT)
+                .isLessThan(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED)
+            assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT).isLessThan(TEST_AUTO_DISMISS_TIME)
+            assertThat(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED)
+                .isLessThan(TEST_AUTO_DISMISS_TIME)
             assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
             assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt
new file mode 100644
index 0000000..75f5de0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 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.notification.people
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.content.pm.ShortcutInfo
+import android.service.notification.NotificationListenerService.Ranking
+import android.service.notification.StatusBarNotification
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PeopleNotificationIdentifierTest : SysuiTestCase() {
+
+    private lateinit var underTest: PeopleNotificationIdentifierImpl
+
+    private val summary1 = notificationEntry("foo", 1, summary = true)
+    private val summary2 = notificationEntry("bar", 1, summary = true)
+    private val entries =
+        listOf<GroupEntry>(
+            GroupEntryBuilder()
+                .setSummary(summary1)
+                .setChildren(
+                    listOf(
+                        notificationEntry("foo", 2),
+                        notificationEntry("foo", 3),
+                        notificationEntry("foo", 4)
+                    )
+                )
+                .build(),
+            GroupEntryBuilder()
+                .setSummary(summary2)
+                .setChildren(
+                    listOf(
+                        notificationEntry("bar", 2),
+                        notificationEntry("bar", 3),
+                        notificationEntry("bar", 4)
+                    )
+                )
+                .build()
+        )
+
+    private fun notificationEntry(
+        pkg: String,
+        id: Int,
+        summary: Boolean = false
+    ): NotificationEntry {
+        val sbn = mock(StatusBarNotification::class.java)
+        Mockito.`when`(sbn.key).thenReturn("key")
+        Mockito.`when`(sbn.notification).thenReturn(mock(Notification::class.java))
+        if (summary)
+            Mockito.`when`(sbn.notification.isGroupSummary).thenReturn(true)
+        return NotificationEntryBuilder().setPkg(pkg)
+            .setId(id)
+            .setSbn(sbn)
+            .build().apply {
+                row = mock(ExpandableNotificationRow::class.java)
+            }
+    }
+
+    private fun personRanking(entry: NotificationEntry, personType: Int): Ranking {
+        val channel = NotificationChannel("person", "person", 4)
+        channel.setConversationId("parent", "person")
+        channel.setImportantConversation(true)
+
+        val br = RankingBuilder(entry.ranking)
+
+        when (personType) {
+            TYPE_NON_PERSON -> br.setIsConversation(false)
+            TYPE_PERSON -> {
+                br.setIsConversation(true)
+                br.setShortcutInfo(null)
+            }
+
+            TYPE_IMPORTANT_PERSON -> {
+                br.setIsConversation(true)
+                br.setShortcutInfo(mock(ShortcutInfo::class.java))
+                br.setChannel(channel)
+            }
+
+            else -> {
+                br.setIsConversation(true)
+                br.setShortcutInfo(mock(ShortcutInfo::class.java))
+            }
+        }
+
+        return br.build()
+    }
+
+    @Before
+    fun setUp() {
+        val personExtractor = object : NotificationPersonExtractor {
+            public override fun isPersonNotification(sbn: StatusBarNotification): Boolean {
+                return true
+            }
+        }
+
+        underTest = PeopleNotificationIdentifierImpl(
+            personExtractor,
+            GroupMembershipManagerImpl()
+        )
+    }
+
+    private val Ranking.personTypeInfo
+        get() = when {
+            !isConversation -> TYPE_NON_PERSON
+            conversationShortcutInfo == null -> TYPE_PERSON
+            channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON
+            else -> TYPE_FULL_PERSON
+        }
+
+    @Test
+    fun getPeopleNotificationType_entryIsImportant() {
+        summary1.setRanking(personRanking(summary1, TYPE_IMPORTANT_PERSON))
+
+        assertThat(underTest.getPeopleNotificationType(summary1)).isEqualTo(TYPE_IMPORTANT_PERSON)
+    }
+
+    @Test
+    fun getPeopleNotificationType_importantChild() {
+        entries.get(0).getChildren().get(0).setRanking(
+            personRanking(entries.get(0).getChildren().get(0), TYPE_IMPORTANT_PERSON)
+        )
+
+        assertThat(entries.get(0).summary?.let { underTest.getPeopleNotificationType(it) })
+            .isEqualTo(TYPE_IMPORTANT_PERSON)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
new file mode 100644
index 0000000..aa6e76d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 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.notification.promoted.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.buildNotificationEntry
+import com.android.systemui.statusbar.notification.buildOngoingCallEntry
+import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry
+import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(
+    PromotedNotificationUi.FLAG_NAME,
+    StatusBarNotifChips.FLAG_NAME,
+    StatusBarChipsModernization.FLAG_NAME,
+    StatusBarRootModernization.FLAG_NAME,
+)
+class PromotedNotificationsInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+    private val Kosmos.underTest by Fixture { promotedNotificationsInteractor }
+
+    @Before
+    fun setUp() {
+        kosmos.statusBarNotificationChipsInteractor.start()
+    }
+
+    @Test
+    fun orderedChipNotificationKeys_containsNonPromotedCalls() =
+        kosmos.runTest {
+            // GIVEN a call and a promoted ongoing notification
+            val callEntry = buildOngoingCallEntry(promoted = false)
+            val ronEntry = buildPromotedOngoingEntry()
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            renderNotificationListInteractor.setRenderedList(
+                listOf(callEntry, ronEntry, otherEntry)
+            )
+
+            val orderedChipNotificationKeys by
+                collectLastValue(underTest.orderedChipNotificationKeys)
+
+            // THEN the order of the notification keys should be the call then the RON
+            assertThat(orderedChipNotificationKeys)
+                .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+        }
+
+    @Test
+    fun orderedChipNotificationKeys_containsPromotedCalls() =
+        kosmos.runTest {
+            // GIVEN a call and a promoted ongoing notification
+            val callEntry = buildOngoingCallEntry(promoted = true)
+            val ronEntry = buildPromotedOngoingEntry()
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            renderNotificationListInteractor.setRenderedList(
+                listOf(callEntry, ronEntry, otherEntry)
+            )
+
+            val orderedChipNotificationKeys by
+                collectLastValue(underTest.orderedChipNotificationKeys)
+
+            // THEN the order of the notification keys should be the call then the RON
+            assertThat(orderedChipNotificationKeys)
+                .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+        }
+
+    @Test
+    fun topPromotedNotificationContent_skipsNonPromotedCalls() =
+        kosmos.runTest {
+            // GIVEN a non-promoted call and a promoted ongoing notification
+            val callEntry = buildOngoingCallEntry(promoted = false)
+            val ronEntry = buildPromotedOngoingEntry()
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            renderNotificationListInteractor.setRenderedList(
+                listOf(callEntry, ronEntry, otherEntry)
+            )
+
+            val topPromotedNotificationContent by
+                collectLastValue(underTest.topPromotedNotificationContent)
+
+            // THEN the ron is first because the call has no content
+            assertThat(topPromotedNotificationContent?.identity?.key)
+                .isEqualTo("0|test_pkg|0|ron|0")
+        }
+
+    @Test
+    fun topPromotedNotificationContent_includesPromotedCalls() =
+        kosmos.runTest {
+            // GIVEN a promoted call and a promoted ongoing notification
+            val callEntry = buildOngoingCallEntry(promoted = true)
+            val ronEntry = buildPromotedOngoingEntry()
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            renderNotificationListInteractor.setRenderedList(
+                listOf(callEntry, ronEntry, otherEntry)
+            )
+
+            val topPromotedNotificationContent by
+                collectLastValue(underTest.topPromotedNotificationContent)
+
+            // THEN the call is the top notification
+            assertThat(topPromotedNotificationContent?.identity?.key)
+                .isEqualTo("0|test_pkg|0|call|0")
+        }
+
+    @Test
+    fun topPromotedNotificationContent_nullWithNoPromotedNotifications() =
+        kosmos.runTest {
+            // GIVEN a a non-promoted call and no promoted ongoing entry
+            val callEntry = buildOngoingCallEntry(promoted = false)
+            val otherEntry = buildNotificationEntry(tag = "other")
+
+            renderNotificationListInteractor.setRenderedList(listOf(callEntry, otherEntry))
+
+            val topPromotedNotificationContent by
+                collectLastValue(underTest.topPromotedNotificationContent)
+
+            // THEN there is no top promoted notification
+            assertThat(topPromotedNotificationContent).isNull()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 7d406b4..9f35d63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -67,6 +67,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
@@ -248,14 +249,13 @@
                 true /* isNewView */, (v, p, r) -> true,
                 new InflationCallback() {
                     @Override
-                    public void handleInflationException(NotificationEntry entry,
-                            Exception e) {
+                    public void handleInflationException(Exception e) {
                         countDownLatch.countDown();
                         throw new RuntimeException("No Exception expected");
                     }
 
                     @Override
-                    public void onAsyncInflationFinished(NotificationEntry entry) {
+                    public void onAsyncInflationFinished() {
                         countDownLatch.countDown();
                     }
                 }, mRow.getPrivateLayout(), null, null, new HashMap<>(),
@@ -539,8 +539,7 @@
         inflater.setInflateSynchronously(true);
         InflationCallback callback = new InflationCallback() {
             @Override
-            public void handleInflationException(NotificationEntry entry,
-                    Exception e) {
+            public void handleInflationException(Exception e) {
                 if (!expectingException) {
                     exceptionHolder.setException(e);
                 }
@@ -548,7 +547,7 @@
             }
 
             @Override
-            public void onAsyncInflationFinished(NotificationEntry entry) {
+            public void onAsyncInflationFinished() {
                 if (expectingException) {
                     exceptionHolder.setException(new RuntimeException(
                             "Inflation finished even though there should be an error"));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 82eca37..ce3aee1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
@@ -223,12 +224,12 @@
             remoteViewClickHandler = { _, _, _ -> true },
             callback =
                 object : InflationCallback {
-                    override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+                    override fun handleInflationException(e: Exception) {
                         countDownLatch.countDown()
                         throw RuntimeException("No Exception expected")
                     }
 
-                    override fun onAsyncInflationFinished(entry: NotificationEntry) {
+                    override fun onAsyncInflationFinished() {
                         countDownLatch.countDown()
                     }
                 },
@@ -675,14 +676,14 @@
             inflater.setInflateSynchronously(true)
             val callback: InflationCallback =
                 object : InflationCallback {
-                    override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+                    override fun handleInflationException(e: Exception) {
                         if (!expectingException) {
                             exceptionHolder.exception = e
                         }
                         countDownLatch.countDown()
                     }
 
-                    override fun onAsyncInflationFinished(entry: NotificationEntry) {
+                    override fun onAsyncInflationFinished() {
                         if (expectingException) {
                             exceptionHolder.exception =
                                 RuntimeException(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index c39b252c..f2131da 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -615,7 +615,7 @@
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
         inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
-                mRowInflaterTaskLogger));
+                mRowInflaterTaskLogger, UserHandle.of(entry.getSbn().getNormalizedUserId())));
         mRow = (ExpandableNotificationRow) inflater.inflate(
                 R.layout.status_bar_notification_row,
                 null /* root */,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index d570f18..6381b4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -57,11 +57,12 @@
             statusBarStateController = mock()
             whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
         }
-    private val underTest = kosmos.notificationShelfViewModel
     private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
     private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController
     private val powerRepository = kosmos.fakePowerRepository
+    private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController }
+
+    private val underTest by lazy { kosmos.notificationShelfViewModel }
 
     @Test
     fun canModifyColorOfNotifications_whenKeyguardNotShowing() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 256da253..9c5d65e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.statusbar.notification.stack
 
+import android.os.UserHandle
 import android.platform.test.annotations.EnableFlags
 import android.service.notification.StatusBarNotification
 import android.testing.TestableLooper.RunWithLooper
@@ -21,6 +22,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
@@ -978,7 +980,10 @@
     ) {
         val sbnMock: StatusBarNotification = mock()
         val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) }
-        val row = ExpandableNotificationRow(mContext, null, mockEntry)
+        val row = when (NotificationBundleUi.isEnabled) {
+            true -> ExpandableNotificationRow(mContext, null, UserHandle.CURRENT)
+            false -> ExpandableNotificationRow(mContext, null, mockEntry)
+        }
         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(row)
         whenever(ambientState.isExpansionChanging).thenReturn(true)
         whenever(ambientState.expansionFraction).thenReturn(expansionFraction)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index 8ec17da..345ddae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -46,9 +46,11 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationContentView;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -133,9 +135,11 @@
         final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class);
         final NotificationContentView privateLayout = mock(NotificationContentView.class);
         final NotificationEntry enrEntry = mock(NotificationEntry.class);
+        final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class);
 
         when(enr.getPrivateLayout()).thenReturn(privateLayout);
         when(enr.getEntry()).thenReturn(enrEntry);
+        when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter);
         when(enr.isChildInGroup()).thenReturn(true);
         when(enr.areChildrenExpanded()).thenReturn(false);
 
@@ -144,7 +148,11 @@
                 enr, mock(View.class), false, onExpandedVisibleRunner);
 
         // THEN
-        verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+        if (NotificationBundleUi.isEnabled()) {
+            verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter);
+        } else {
+            verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+        }
         verify(enr).setUserExpanded(true);
         verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
     }
@@ -169,7 +177,8 @@
                 enr, mock(View.class), false, onExpandedVisibleRunner);
 
         // THEN
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
         verify(enr).setUserExpanded(true);
         verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
     }
@@ -193,7 +202,8 @@
                 enr, mock(View.class), false, onExpandedVisibleRunner);
 
         // THEN
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
         verify(enr).setUserExpanded(true);
         verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
     }
@@ -207,9 +217,11 @@
         final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class);
         final NotificationContentView privateLayout = mock(NotificationContentView.class);
         final NotificationEntry enrEntry = mock(NotificationEntry.class);
+        final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class);
 
         when(enr.getPrivateLayout()).thenReturn(privateLayout);
         when(enr.getEntry()).thenReturn(enrEntry);
+        when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter);
         when(enr.isChildInGroup()).thenReturn(true);
         when(enr.areChildrenExpanded()).thenReturn(false);
 
@@ -218,7 +230,11 @@
                 enr, mock(View.class), false, onExpandedVisibleRunner);
 
         // THEN
-        verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+        if (NotificationBundleUi.isEnabled()) {
+            verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter);
+        } else {
+            verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+        }
         verify(enr, never()).setUserExpanded(anyBoolean());
         verify(privateLayout, never()).setOnExpandedVisibleListener(any());
     }
@@ -244,6 +260,7 @@
 
         // THEN
         verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry);
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
         verify(enr, never()).setUserExpanded(anyBoolean());
         verify(privateLayout, never()).setOnExpandedVisibleListener(any());
     }
@@ -272,7 +289,8 @@
         verify(enr).toggleExpansionState();
         verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
         verify(enr, never()).setUserExpanded(anyBoolean());
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
     }
 
     @Test
@@ -299,7 +317,8 @@
         verify(enr, never()).toggleExpansionState();
         verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner);
         verify(enr, never()).setUserExpanded(anyBoolean());
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
     }
 
     @Test
@@ -326,7 +345,8 @@
         verify(enr).toggleExpansionState();
         verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
         verify(enr, never()).setUserExpanded(anyBoolean());
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
     }
 
     @Test
@@ -353,6 +373,7 @@
         verify(enr, never()).toggleExpansionState();
         verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner);
         verify(enr, never()).setUserExpanded(anyBoolean());
-        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+        verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index 183cd8f..eb961bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -57,7 +57,7 @@
     override val ongoingActivityChipsLegacy =
         MutableStateFlow(MultipleOngoingActivityChipsModelLegacy())
 
-    override val statusBarPopupChips = MutableStateFlow(emptyList<PopupChipModel.Shown>())
+    override val popupChips = emptyList<PopupChipModel.Shown>()
 
     override val mediaProjectionStopDialogDueToCallEndedState =
         MutableStateFlow(MediaProjectionStopDialogModel.Hidden)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index ff1ffcc..22e28d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -469,7 +469,7 @@
         }
 
     @Test
-    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
     fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
         kosmos.runTest {
             val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java
new file mode 100644
index 0000000..ecd04a4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2025 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.theme;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+
+public class HardwareColorRule implements TestRule {
+    public String color = "";
+    public String[] options = {};
+    public boolean isTesting = false;
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class);
+        if (hardwareColors != null) {
+            color = hardwareColors.color();
+            options = hardwareColors.options();
+            isTesting = true;
+        }
+        return base;
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java
new file mode 100644
index 0000000..0b8df2e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 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.theme;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface HardwareColors {
+    String color();
+    String[] options();
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 5cd0846..9a0b812 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -64,6 +64,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.monet.DynamicColors;
@@ -77,6 +78,7 @@
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -98,6 +100,9 @@
     private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100);
     private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101);
 
+    @Rule
+    public HardwareColorRule rule = new HardwareColorRule();
+
     @Mock
     private JavaAdapter mJavaAdapter;
     @Mock
@@ -148,13 +153,17 @@
     @Captor
     private ArgumentCaptor<ContentObserver> mSettingsObserver;
 
+    @Mock
+    private SystemPropertiesHelper mSystemProperties;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+
         when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true);
         when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
         when(mUiModeManager.getContrast()).thenReturn(0.5f);
-        when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
+
         when(mResources.getColor(eq(android.R.color.system_accent1_500), any()))
                 .thenReturn(Color.RED);
         when(mResources.getColor(eq(android.R.color.system_accent2_500), any()))
@@ -166,11 +175,20 @@
         when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
                 .thenReturn(Color.BLACK);
 
+        when(mResources.getStringArray(com.android.internal.R.array.theming_defaults))
+                .thenReturn(rule.options);
+
+        // should fallback to `*|TONAL_SPOT|home_wallpaper`
+        when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color);
+        // will try set hardware colors as boot ONLY if user is not set yet
+        when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting);
+
         mThemeOverlayController = new ThemeOverlayController(mContext,
                 mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+                mSystemProperties) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
@@ -214,12 +232,59 @@
     public void start_checksWallpaper() {
         ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
         verify(mBgExecutor).execute(registrationRunnable.capture());
-
         registrationRunnable.getValue().run();
         verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
     }
 
     @Test
+    @HardwareColors(color = "BLK", options = {
+            "BLK|MONOCHROMATIC|#FF0000",
+            "*|VIBRANT|home_wallpaper"
+    })
+    @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+    public void start_checkHardwareColor() {
+        // getWallpaperColors should not be called
+        ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+        verify(mMainExecutor).execute(registrationRunnable.capture());
+        registrationRunnable.getValue().run();
+        verify(mWallpaperManager, never()).getWallpaperColors(anyInt());
+
+        assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC);
+        assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get(
+                0).toArgb()).isEqualTo(Color.RED);
+    }
+
+    @Test
+    @HardwareColors(color = "", options = {
+            "BLK|MONOCHROMATIC|#FF0000",
+            "*|VIBRANT|home_wallpaper"
+    })
+    @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+    public void start_wildcardColor() {
+        // getWallpaperColors will be called because we srt wildcard to `home_wallpaper`
+        ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+        verify(mMainExecutor).execute(registrationRunnable.capture());
+        registrationRunnable.getValue().run();
+        verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
+
+        assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT);
+    }
+
+    @Test
+    @HardwareColors(color = "NONEXISTENT", options = {})
+    @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+    public void start_fallbackColor() {
+        // getWallpaperColors will be called because we default color source is `home_wallpaper`
+        ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+        verify(mMainExecutor).execute(registrationRunnable.capture());
+        registrationRunnable.getValue().run();
+        verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
+
+        assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT);
+    }
+
+
+    @Test
     public void onWallpaperColorsChanged_setsTheme_whenForeground() {
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
@@ -287,9 +352,9 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.system_palette\":\"override.package.name\","
-                        + "\"android.theme.customization.color_source\":\"preset\"}";
+        String jsonString = createJsonString(TestColorSource.preset, "override.package.name",
+                "TONAL_SPOT");
+
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -313,11 +378,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -348,11 +409,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -381,11 +438,7 @@
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.lock_wallpaper);
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -404,11 +457,7 @@
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.lock_wallpaper);
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -455,8 +504,8 @@
     @Test
     public void onSettingChanged_invalidStyle() {
         when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
-        String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\","
-                + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00",
+                "some_invalid_name");
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -473,11 +522,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -506,11 +551,7 @@
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -537,11 +578,7 @@
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -570,11 +607,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -599,7 +632,6 @@
     }
 
 
-
     @Test
     @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() {
@@ -608,11 +640,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(0xffa16b00), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -642,11 +670,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -676,11 +700,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -711,11 +731,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(0xffa16b00), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -745,11 +761,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
-                        + "\"android.theme.customization.system_palette\":\"A16B00\","
-                        + "\"android.theme.customization.accent_color\":\"A16B00\","
-                        + "\"android.theme.customization.color_index\":\"2\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper);
 
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -886,7 +898,8 @@
                 mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+                mSystemProperties) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
@@ -926,7 +939,8 @@
                 mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+                mSystemProperties) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
@@ -992,7 +1006,7 @@
         clearInvocations(mThemeOverlayApplier);
 
         // Device went to sleep and second set of colors was applied.
-        mainColors =  new WallpaperColors(Color.valueOf(Color.BLUE),
+        mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
                 Color.valueOf(Color.RED), null);
         mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
                 USER_SYSTEM);
@@ -1018,7 +1032,7 @@
         clearInvocations(mThemeOverlayApplier);
 
         // Device went to sleep and second set of colors was applied.
-        mainColors =  new WallpaperColors(Color.valueOf(Color.BLUE),
+        mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
                 Color.valueOf(Color.RED), null);
         mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
                 USER_SYSTEM);
@@ -1034,8 +1048,9 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
 
-        String jsonString =
-                "{\"android.theme.customization.system_palette\":\"00FF00\"}";
+        String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00",
+                "TONAL_SPOT");
+
         when(mSecureSettings.getStringForUser(
                 eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
                 .thenReturn(jsonString);
@@ -1115,4 +1130,25 @@
                         + DynamicColors.getCustomColorsMapped(false).size() * 2)
         ).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null));
     }
+
+    private enum TestColorSource {
+        preset,
+        home_wallpaper,
+        lock_wallpaper
+    }
+
+    private String createJsonString(TestColorSource colorSource, String seedColorHex,
+            String style) {
+        return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\","
+                + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\","
+                + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\","
+                + "\"android.theme.customization.color_index\":\"2\","
+                + "\"android.theme.customization.theme_style\":\"" + style + "\"}";
+    }
+
+    private String createJsonString(TestColorSource colorSource) {
+        return createJsonString(colorSource, "A16B00", "TONAL_SPOT");
+    }
+
+
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
index e484d80..04ab988 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
@@ -19,21 +19,17 @@
 import android.bluetooth.BluetoothDevice
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.uiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.android.systemui.volume.data.repository.audioSharingRepository
-import com.android.systemui.volume.domain.interactor.audioSharingInteractor
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
@@ -43,47 +39,30 @@
 class AudioSharingStreamSliderViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
 
-    private lateinit var stream: AudioSharingStreamSliderViewModel
-
-    @Before
-    fun setUp() {
-        stream = audioSharingStreamSliderViewModel()
-    }
-
-    private fun audioSharingStreamSliderViewModel(): AudioSharingStreamSliderViewModel {
-        return AudioSharingStreamSliderViewModel(
-            testScope.backgroundScope,
-            context,
-            kosmos.audioSharingInteractor,
-            kosmos.uiEventLogger,
-            kosmos.sliderHapticsViewModelFactory,
-        )
-    }
+    private val underTest: AudioSharingStreamSliderViewModel =
+        with(kosmos) { audioSharingStreamSliderViewModelFactory.create(applicationCoroutineScope) }
 
     @Test
     fun slider_media_inAudioSharing() =
-        with(kosmos) {
-            testScope.runTest {
-                val audioSharingSlider by collectLastValue(stream.slider)
+        kosmos.runTest {
+            val audioSharingSlider by collectLastValue(underTest.slider)
 
-                val bluetoothDevice: BluetoothDevice = mock {}
-                val cachedDevice: CachedBluetoothDevice = mock {
-                    on { groupId }.thenReturn(123)
-                    on { device }.thenReturn(bluetoothDevice)
-                    on { name }.thenReturn("my headset 2")
-                }
-                audioSharingRepository.setSecondaryDevice(cachedDevice)
-
-                audioSharingRepository.setInAudioSharing(true)
-                audioSharingRepository.setSecondaryGroupId(123)
-
-                runCurrent()
-
-                assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2")
-                assertThat(audioSharingSlider!!.icon)
-                    .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null))
+            val bluetoothDevice: BluetoothDevice = mock {}
+            val cachedDevice: CachedBluetoothDevice = mock {
+                on { groupId }.thenReturn(123)
+                on { device }.thenReturn(bluetoothDevice)
+                on { name }.thenReturn("my headset 2")
             }
+            audioSharingRepository.setSecondaryDevice(cachedDevice)
+
+            audioSharingRepository.setInAudioSharing(true)
+            audioSharingRepository.setSecondaryGroupId(123)
+
+            runCurrent()
+
+            assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2")
+            assertThat(audioSharingSlider!!.icon)
+                .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null))
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index 7c166de..cc6a7b9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -42,7 +42,6 @@
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -53,7 +52,6 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class WallpaperRepositoryImplTest : SysuiTestCase() {
-
     private var isWallpaperSupported = true
     private val kosmos =
         testKosmos().apply {
@@ -293,12 +291,9 @@
                 Intent(Intent.ACTION_WALLPAPER_CHANGED),
             )
             assertThat(latest).isTrue()
-            assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
-            assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
         }
 
     @Test
-    @Ignore("ag/31591766")
     @EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS)
     fun shouldSendNotificationLayout_setNotExtendedEffectsWallpaper_cancelSendLayoutJob() =
         testScope.runTest {
@@ -315,8 +310,6 @@
                 Intent(Intent.ACTION_WALLPAPER_CHANGED),
             )
             assertThat(latest).isTrue()
-            assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
-            assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
 
             whenever(kosmos.wallpaperManager.getWallpaperInfoForUser(any()))
                 .thenReturn(UNSUPPORTED_WP)
@@ -327,7 +320,6 @@
             runCurrent()
 
             assertThat(latest).isFalse()
-            assertThat(underTest.sendLockscreenLayoutJob?.isCancelled).isEqualTo(true)
         }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
index cd6e18a..31a611c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
@@ -30,13 +30,8 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.testKosmos
-import com.android.systemui.wallpapers.data.repository.fakeWallpaperFocalAreaRepository
 import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.wallpaperRepository
 import com.android.systemui.wallpapers.ui.viewmodel.wallpaperFocalAreaViewModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -77,40 +72,26 @@
             .thenReturn(2f)
         underTest =
             WallpaperFocalAreaInteractor(
-                applicationScope = testScope.backgroundScope,
                 context = kosmos.mockedContext,
-                wallpaperFocalAreaRepository = kosmos.fakeWallpaperFocalAreaRepository,
+                wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository,
                 shadeRepository = kosmos.shadeRepository,
-                activeNotificationsInteractor = kosmos.activeNotificationsInteractor,
-                wallpaperRepository = kosmos.wallpaperRepository,
             )
     }
 
-    private fun overrideMockedResources(overrideResources: OverrideResources) {
-        val displayMetrics =
-            DisplayMetrics().apply {
-                widthPixels = overrideResources.screenWidth
-                heightPixels = overrideResources.screenHeight
-                density = 2f
-            }
-        whenever(mockedResources.displayMetrics).thenReturn(displayMetrics)
-        whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape))
-            .thenReturn(overrideResources.centerAlignFocalArea)
-    }
-
     @Test
     fun focalAreaBounds_withoutNotifications_inHandheldDevices() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1000,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(false)
-            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
@@ -122,15 +103,15 @@
     fun focalAreaBounds_withNotifications_inHandheldDevices() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1000,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(false)
-            kosmos.activeNotificationListRepository.setActiveNotifs(1)
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -139,58 +120,38 @@
         }
 
     @Test
-    fun focalAreaBounds_withNotifications_inUnfoldLandscape() =
+    fun focalAreaBounds_inUnfoldLandscape() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 2000,
                     screenHeight = 1600,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(true)
-            kosmos.activeNotificationListRepository.setActiveNotifs(1)
-            kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F)
-            kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
-            kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
-
-            assertThat(bounds).isEqualTo(RectF(500f, 600F, 1000F, 1100F))
-        }
-
-    @Test
-    fun focalAreaBounds_withoutNotifications_inUnfoldLandscape() =
-        testScope.runTest {
-            overrideMockedResources(
-                OverrideResources(
-                    screenWidth = 2000,
-                    screenHeight = 1600,
-                    centerAlignFocalArea = false,
-                )
-            )
-            val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
-            kosmos.shadeRepository.setShadeLayoutWide(true)
-            kosmos.activeNotificationListRepository.setActiveNotifs(0)
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
 
-            assertThat(bounds).isEqualTo(RectF(1000f, 600F, 1500F, 1100F))
+            assertThat(bounds).isEqualTo(RectF(600f, 600F, 1400F, 1100F))
         }
 
     @Test
     fun focalAreaBounds_withNotifications_inUnfoldPortrait() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1600,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(false)
-            kosmos.activeNotificationListRepository.setActiveNotifs(1)
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -202,15 +163,15 @@
     fun focalAreaBounds_withoutNotifications_inUnfoldPortrait() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1600,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(false)
-            kosmos.activeNotificationListRepository.setActiveNotifs(0)
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -219,18 +180,18 @@
         }
 
     @Test
-    fun focalAreaBounds_withNotifications_inTabletLandscape() =
+    fun focalAreaBounds_inTabletLandscape() =
         testScope.runTest {
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 3000,
                     screenHeight = 2000,
                     centerAlignFocalArea = true,
-                )
+                ),
             )
             val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
             kosmos.shadeRepository.setShadeLayoutWide(true)
-            kosmos.activeNotificationListRepository.setActiveNotifs(1)
             kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
             kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(200F)
             kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(200F)
@@ -239,35 +200,16 @@
         }
 
     @Test
-    fun focalAreaBounds_withoutNotifications_inTabletLandscape() =
-        testScope.runTest {
-            overrideMockedResources(
-                OverrideResources(
-                    screenWidth = 3000,
-                    screenHeight = 2000,
-                    centerAlignFocalArea = true,
-                )
-            )
-            val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
-            kosmos.shadeRepository.setShadeLayoutWide(true)
-            kosmos.activeNotificationListRepository.setActiveNotifs(0)
-            kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
-            kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
-            kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
-
-            assertThat(bounds).isEqualTo(RectF(1000f, 600F, 2000F, 1400F))
-        }
-
-    @Test
     fun onTap_inFocalBounds() =
         testScope.runTest {
             kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F))
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1000,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds(
                 RectF(250f, 700F, 750F, 1400F)
@@ -287,11 +229,12 @@
         testScope.runTest {
             kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F))
             overrideMockedResources(
+                mockedResources,
                 OverrideResources(
                     screenWidth = 1000,
                     screenHeight = 2000,
                     centerAlignFocalArea = false,
-                )
+                ),
             )
             kosmos.wallpaperFocalAreaViewModel = mock()
             kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds(
@@ -309,4 +252,21 @@
         val screenHeight: Int,
         val centerAlignFocalArea: Boolean,
     )
+
+    companion object {
+        fun overrideMockedResources(
+            mockedResources: Resources,
+            overrideResources: OverrideResources,
+        ) {
+            val displayMetrics =
+                DisplayMetrics().apply {
+                    widthPixels = overrideResources.screenWidth
+                    heightPixels = overrideResources.screenHeight
+                    density = 2f
+                }
+            whenever(mockedResources.displayMetrics).thenReturn(displayMetrics)
+            whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape))
+                .thenReturn(overrideResources.centerAlignFocalArea)
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt
new file mode 100644
index 0000000..3cd2072
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2025 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.wallpapers.ui.viewmodel
+
+import android.content.mockedContext
+import android.content.res.Resources
+import android.graphics.RectF
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.testKosmos
+import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.Companion.overrideMockedResources
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.OverrideResources
+import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WallpaperFocalAreaViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private lateinit var mockedResources: Resources
+    lateinit var underTest: WallpaperFocalAreaViewModel
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mockedResources = mock<Resources>()
+        overrideMockedResources(
+            mockedResources,
+            OverrideResources(screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false),
+        )
+        whenever(kosmos.mockedContext.resources).thenReturn(mockedResources)
+        whenever(
+                mockedResources.getFloat(
+                    Resources.getSystem()
+                        .getIdentifier(
+                            /* name= */ "config_wallpaperMaxScale",
+                            /* defType= */ "dimen",
+                            /* defPackage= */ "android",
+                        )
+                )
+            )
+            .thenReturn(2f)
+        kosmos.wallpaperFocalAreaInteractor =
+            WallpaperFocalAreaInteractor(
+                context = kosmos.mockedContext,
+                wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository,
+                shadeRepository = kosmos.shadeRepository,
+            )
+        underTest =
+            WallpaperFocalAreaViewModel(
+                wallpaperFocalAreaInteractor = kosmos.wallpaperFocalAreaInteractor,
+                keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
+            )
+    }
+
+    @Test
+    fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() =
+        testScope.runTest {
+            overrideMockedResources(
+                mockedResources,
+                OverrideResources(
+                    screenWidth = 1600,
+                    screenHeight = 2000,
+                    centerAlignFocalArea = false,
+                ),
+            )
+            val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN),
+                    TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN),
+                ),
+                testScope,
+            )
+
+            setTestFocalAreaBounds()
+
+            assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F))
+        }
+
+    @Test
+    fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() =
+        testScope.runTest {
+            overrideMockedResources(
+                mockedResources,
+                OverrideResources(
+                    screenWidth = 1600,
+                    screenHeight = 2000,
+                    centerAlignFocalArea = false,
+                ),
+            )
+            val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)),
+                testScope,
+            )
+            setTestFocalAreaBounds()
+
+            assertThat(bounds).isEqualTo(null)
+        }
+
+    private fun setTestFocalAreaBounds() {
+        kosmos.shadeRepository.setShadeLayoutWide(false)
+        kosmos.activeNotificationListRepository.setActiveNotifs(0)
+        kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F)
+        kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F)
+        kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F)
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 9a83744..3ed321e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -83,6 +83,13 @@
         }
     }
 
+    fun updateAxes(lsFVar: String, aodFVar: String) {
+        i({ "updateAxes(LS = $str1, AOD = $str2)" }) {
+            str1 = lsFVar
+            str2 = aodFVar
+        }
+    }
+
     fun addView(child: View) {
         d({ "addView($str1 @$int1)" }) {
             str1 = child::class.simpleName!!
@@ -90,6 +97,14 @@
         }
     }
 
+    fun animateDoze() {
+        d("animateDoze()")
+    }
+
+    fun animateCharge() {
+        d("animateCharge()")
+    }
+
     fun animateFidget(x: Float, y: Float) {
         d({ "animateFidget($str1, $str2)" }) {
             str1 = x.toString()
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml
index 61d6a904..a27e29f 100644
--- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml
@@ -19,12 +19,13 @@
     android:height="40dp"
     android:viewportHeight="40"
     android:viewportWidth="40">
+  <group>
+    <clip-path android:pathData="M8,12h24.5v15.5h-24.5z" />
     <path
-        android:fillColor="#F7DAEE"
-        android:fillType="evenOdd"
-        android:pathData="M20.76,19C21.65,19 22.096,17.924 21.467,17.294L19.284,15.105C18.895,14.716 18.895,14.085 19.285,13.695C19.674,13.306 20.306,13.306 20.695,13.695L26.293,19.293C26.683,19.683 26.683,20.317 26.293,20.707L20.705,26.295C20.315,26.685 19.683,26.686 19.292,26.298C18.9,25.907 18.898,25.272 19.29,24.88L21.463,22.707C22.093,22.077 21.647,21 20.756,21H10C9.448,21 9,20.552 9,20C9,19.448 9.448,19 10,19H20.76ZM32,26C32,26.552 31.552,27 31,27C30.448,27 30,26.552 30,26V14C30,13.448 30.448,13 31,13C31.552,13 32,13.448 32,14V26Z"
-        android:strokeColor="#F7DAEE"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round"
-        android:strokeWidth="2" />
+        android:fillColor="#000000"
+        android:pathData="M30.75,12C29.79,12 29,12.79 29,13.75V25.75C29,26.71 29.79,27.5 30.75,27.5C31.71,27.5 32.5,26.71 32.5,25.75V13.75C32.5,12.79 31.71,12 30.75,12Z" />
+    <path
+        android:fillColor="#000000"
+        android:pathData="M20.98,12.92C20.3,12.24 19.19,12.24 18.51,12.92C17.83,13.6 17.83,14.71 18.51,15.39L21.12,18H9.75C8.79,18 8,18.79 8,19.75C8,20.71 8.79,21.5 9.75,21.5H21.11L18.51,24.1C18.18,24.43 18,24.87 18,25.34C18,25.81 18.18,26.25 18.52,26.58C18.86,26.92 19.31,27.09 19.75,27.09C20.19,27.09 20.65,26.92 20.99,26.58L26.61,20.96C27.28,20.29 27.28,19.21 26.61,18.55L20.98,12.92Z" />
+  </group>
 </vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml
deleted file mode 100644
index 044656d..0000000
--- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-  ~ Copyright (C) 2025 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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="40dp"
-    android:height="40dp"
-    android:viewportHeight="40"
-    android:viewportWidth="40">
-    <path
-        android:fillColor="#ECDFE5"
-        android:pathData="M18.792,26.5L23.333,21.958L27.875,26.5L29.875,24.542L25.292,20L29.792,15.458L27.833,13.5L23.333,18.042L18.792,13.5L16.792,15.458L21.375,20L16.792,24.542L18.792,26.5ZM14.708,33.333C14.292,33.333 13.875,33.236 13.458,33.042C13.069,32.847 12.75,32.569 12.5,32.208L3.333,20L12.458,7.792C12.708,7.431 13.028,7.153 13.417,6.958C13.833,6.764 14.264,6.667 14.708,6.667H33.917C34.694,6.667 35.347,6.944 35.875,7.5C36.431,8.028 36.708,8.681 36.708,9.458V30.542C36.708,31.319 36.431,31.986 35.875,32.542C35.347,33.069 34.694,33.333 33.917,33.333H14.708Z" />
-</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml
new file mode 100644
index 0000000..86f95bc
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="40dp"
+    android:viewportHeight="40"
+    android:viewportWidth="40">
+    <path
+        android:fillColor="#000000"
+        android:pathData="M22.167,21.9L25.531,25.265C25.795,25.502 26.112,25.621 26.481,25.621C26.851,25.621 27.167,25.502 27.431,25.265C27.669,25.001 27.788,24.684 27.788,24.315C27.788,23.919 27.669,23.589 27.431,23.325L24.067,20L27.392,16.675C27.656,16.411 27.788,16.094 27.788,15.725C27.788,15.356 27.656,15.039 27.392,14.775C27.128,14.511 26.798,14.379 26.402,14.379C26.033,14.379 25.729,14.511 25.492,14.775L22.167,18.1L18.802,14.735C18.538,14.498 18.222,14.379 17.852,14.379C17.483,14.379 17.166,14.498 16.902,14.735C16.665,14.999 16.546,15.329 16.546,15.725C16.546,16.094 16.665,16.411 16.902,16.675L20.267,20L16.902,23.325C16.665,23.589 16.546,23.906 16.546,24.275C16.546,24.644 16.665,24.961 16.902,25.225C17.166,25.489 17.483,25.621 17.852,25.621C18.248,25.621 18.578,25.489 18.842,25.225L22.167,21.9ZM14.012,32.667C13.59,32.667 13.181,32.574 12.785,32.39C12.416,32.179 12.099,31.915 11.835,31.598L4.394,21.623C4.024,21.148 3.84,20.607 3.84,20C3.84,19.393 4.024,18.852 4.394,18.377L11.835,8.402C12.073,8.085 12.39,7.835 12.785,7.65C13.181,7.439 13.59,7.333 14.012,7.333H32.142C32.907,7.333 33.554,7.597 34.081,8.125C34.609,8.653 34.873,9.286 34.873,10.025V29.975C34.873,30.714 34.609,31.347 34.081,31.875C33.554,32.403 32.907,32.667 32.142,32.667H14.012Z" />
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml
new file mode 100644
index 0000000..7f551f4
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2025 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="40dp"
+    android:viewportHeight="40"
+    android:viewportWidth="40">
+    <group>
+        <clip-path android:pathData="M5,7h29.89v25h-29.89z" />
+        <path
+            android:fillColor="#000000"
+            android:pathData="M30.96,32H15.59C14.21,32 12.89,31.34 12.06,30.24L5.78,21.86C4.74,20.47 4.74,18.54 5.78,17.15L12.06,8.77C12.89,7.67 14.21,7 15.59,7H30.96C33.13,7 34.89,8.76 34.89,10.93V28.08C34.89,30.25 33.13,32.01 30.96,32.01V32ZM14.46,28.44C14.73,28.79 15.15,29 15.59,29H30.96C31.47,29 31.89,28.58 31.89,28.07V10.93C31.89,10.42 31.47,10 30.96,10H15.59C15.15,10 14.73,10.21 14.46,10.56L8.18,18.94C7.93,19.27 7.93,19.72 8.18,20.05L14.46,28.43V28.44Z" />
+        <path
+            android:fillColor="#000000"
+            android:pathData="M22.46,21.27L25.36,24.17C25.6,24.43 25.89,24.56 26.25,24.56C26.61,24.56 26.9,24.43 27.14,24.17C27.4,23.93 27.53,23.64 27.53,23.28C27.53,22.92 27.4,22.63 27.14,22.39L24.24,19.49L27.14,16.59C27.38,16.35 27.49,16.06 27.49,15.7C27.49,15.34 27.37,15.05 27.14,14.81C26.91,14.57 26.61,14.46 26.25,14.46C25.89,14.46 25.59,14.58 25.33,14.81L22.46,17.71L19.56,14.81C19.32,14.55 19.03,14.42 18.67,14.42C18.31,14.42 18.02,14.55 17.78,14.81C17.52,15.05 17.39,15.34 17.39,15.7C17.39,16.06 17.52,16.35 17.78,16.59L20.68,19.49L17.78,22.39C17.52,22.63 17.39,22.92 17.39,23.28C17.39,23.64 17.52,23.93 17.78,24.17C18.02,24.41 18.31,24.52 18.67,24.52C19.03,24.52 19.32,24.4 19.56,24.17L22.46,21.27Z" />
+    </group>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
index 0b35559..87d06bf 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
@@ -21,7 +21,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/keyguard_lock_padding"
-        android:importantForAccessibility="no"
+        android:accessibilityLiveRegion="polite"
         android:ellipsize="marquee"
         android:focusable="false"
         android:gravity="center"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
index f231df2..c7f320c 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
@@ -67,6 +67,7 @@
             <com.android.systemui.bouncer.ui.BouncerMessageView
                 android:id="@+id/bouncer_message_view"
                 android:screenReaderFocusable="true"
+                android:accessibilityLiveRegion="polite"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 0445722..9359838 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -32,6 +32,7 @@
     <com.android.systemui.bouncer.ui.BouncerMessageView
         android:id="@+id/bouncer_message_view"
         android:screenReaderFocusable="true"
+        android:accessibilityLiveRegion="polite"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
index b184344..6cbe96a 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
@@ -68,6 +68,7 @@
             <com.android.systemui.bouncer.ui.BouncerMessageView
                 android:id="@+id/bouncer_message_view"
                 android:screenReaderFocusable="true"
+                android:accessibilityLiveRegion="polite"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index 0e15ff66..cf38887 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -36,6 +36,7 @@
     <com.android.systemui.bouncer.ui.BouncerMessageView
         android:id="@+id/bouncer_message_view"
         android:screenReaderFocusable="true"
+        android:accessibilityLiveRegion="polite"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
index f6ac02a..33eab17 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
@@ -75,6 +75,7 @@
             <com.android.systemui.bouncer.ui.BouncerMessageView
                 android:id="@+id/bouncer_message_view"
                 android:screenReaderFocusable="true"
+                android:accessibilityLiveRegion="polite"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index ba4da79..fd5eeb8 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -33,6 +33,7 @@
     <com.android.systemui.bouncer.ui.BouncerMessageView
         android:id="@+id/bouncer_message_view"
         android:screenReaderFocusable="true"
+        android:accessibilityLiveRegion="polite"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index bfb37a0d..6d44645 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -31,6 +31,10 @@
     <!-- height for the keyguard pin input field -->
     <dimen name="keyguard_pin_field_height">56dp</dimen>
 
+    <dimen name="keyguard_pattern_dot_size">16dp</dimen>
+    <dimen name="keyguard_pattern_activated_dot_size">24dp</dimen>
+    <dimen name="keyguard_pattern_stroke_width">32dp</dimen>
+
     <!-- height for the keyguard password input field -->
     <dimen name="keyguard_password_field_height">56dp</dimen>
 
diff --git a/packages/SystemUI/res/drawable/ic_legacy_speaker_mute.xml b/packages/SystemUI/res/drawable/ic_legacy_speaker_mute.xml
new file mode 100644
index 0000000..4e402cf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_legacy_speaker_mute.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary"
+    android:autoMirrored="true">
+    <path android:fillColor="#FFFFFFFF"
+        android:pathData="M19.8,22.6 L16.775,19.575Q16.15,19.975 15.45,20.263Q14.75,20.55 14,20.725V18.675Q14.35,18.55 14.688,18.425Q15.025,18.3 15.325,18.125L12,14.8V20L7,15H3V9H6.2L1.4,4.2L2.8,2.8L21.2,21.2ZM19.6,16.8 L18.15,15.35Q18.575,14.575 18.788,13.725Q19,12.875 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,13.3 20.638,14.525Q20.275,15.75 19.6,16.8ZM16.25,13.45 L14,11.2V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,12.375 16.438,12.738Q16.375,13.1 16.25,13.45ZM12,9.2 L9.4,6.6 12,4ZM10,15.15V12.8L8.2,11H5V13H7.85ZM9.1,11.9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_legacy_speaker_on.xml b/packages/SystemUI/res/drawable/ic_legacy_speaker_on.xml
new file mode 100644
index 0000000..2a90e05
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_legacy_speaker_on.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary"
+    android:autoMirrored="true">
+    <path android:fillColor="#FFFFFFFF"
+        android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,13.275 15.838,14.362Q15.175,15.45 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_legacy_volume_ringer_vibrate.xml b/packages/SystemUI/res/drawable/ic_legacy_volume_ringer_vibrate.xml
new file mode 100644
index 0000000..b18e0a7
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_legacy_volume_ringer_vibrate.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="19dp"
+    android:width="19dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:tint="?android:attr/textColorPrimary">
+
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M1,9h2v6H1V9zM4,17h2V7H4V17zM21,9v6h2V9H21zM18,17h2V7h-2V17zM17,5.5v13c0,0.83 -0.67,1.5 -1.5,1.5h-7C7.67,20 7,19.33 7,18.5v-13C7,4.67 7.67,4 8.5,4h7C16.33,4 17,4.67 17,5.5zM15,6H9v12h6V6z"/>
+
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_speaker_mute.xml b/packages/SystemUI/res/drawable/ic_speaker_mute.xml
index 4e402cf..bf31580 100644
--- a/packages/SystemUI/res/drawable/ic_speaker_mute.xml
+++ b/packages/SystemUI/res/drawable/ic_speaker_mute.xml
@@ -1,5 +1,5 @@
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2025 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.
@@ -14,12 +14,15 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?android:attr/textColorPrimary"
-    android:autoMirrored="true">
-    <path android:fillColor="#FFFFFFFF"
-        android:pathData="M19.8,22.6 L16.775,19.575Q16.15,19.975 15.45,20.263Q14.75,20.55 14,20.725V18.675Q14.35,18.55 14.688,18.425Q15.025,18.3 15.325,18.125L12,14.8V20L7,15H3V9H6.2L1.4,4.2L2.8,2.8L21.2,21.2ZM19.6,16.8 L18.15,15.35Q18.575,14.575 18.788,13.725Q19,12.875 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,13.3 20.638,14.525Q20.275,15.75 19.6,16.8ZM16.25,13.45 L14,11.2V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,12.375 16.438,12.738Q16.375,13.1 16.25,13.45ZM12,9.2 L9.4,6.6 12,4ZM10,15.15V12.8L8.2,11H5V13H7.85ZM9.1,11.9Z"/>
-</vector>
\ No newline at end of file
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+  <group>
+    <clip-path
+        android:pathData="M0,0h20v20h-20z"/>
+    <path
+        android:pathData="M16,18.125L13.771,15.896C13.465,16.09 13.097,16.278 12.667,16.458C12.25,16.625 11.861,16.75 11.5,16.833V15.292C11.667,15.222 11.861,15.146 12.083,15.063C12.319,14.965 12.514,14.875 12.667,14.792L10,12.125V16.021L6,12.021H3V8.021H5.875L1.875,4L2.938,2.938L17.063,17.063L16,18.125ZM15.875,13.771L14.792,12.688C15.014,12.285 15.188,11.861 15.313,11.417C15.438,10.958 15.5,10.493 15.5,10.021C15.5,8.785 15.125,7.688 14.375,6.729C13.639,5.757 12.681,5.09 11.5,4.729V3.188C13.125,3.507 14.444,4.313 15.458,5.604C16.486,6.896 17,8.368 17,10.021C17,10.688 16.903,11.34 16.708,11.979C16.514,12.604 16.236,13.201 15.875,13.771ZM13.292,11.188L11.5,9.396V6.854C12.125,7.132 12.611,7.563 12.958,8.146C13.319,8.715 13.5,9.34 13.5,10.021C13.5,10.215 13.486,10.41 13.458,10.604C13.431,10.799 13.375,10.993 13.292,11.188ZM10,7.896L8.063,5.958L10,4.021V7.896Z"
+        android:fillColor="#ECDFE5"/>
+  </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_speaker_on.xml b/packages/SystemUI/res/drawable/ic_speaker_on.xml
index 2a90e05..f0d057e 100644
--- a/packages/SystemUI/res/drawable/ic_speaker_on.xml
+++ b/packages/SystemUI/res/drawable/ic_speaker_on.xml
@@ -1,5 +1,5 @@
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2025 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.
@@ -14,12 +14,15 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?android:attr/textColorPrimary"
-    android:autoMirrored="true">
-    <path android:fillColor="#FFFFFFFF"
-        android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,13.275 15.838,14.362Q15.175,15.45 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
-</vector>
\ No newline at end of file
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+  <group>
+    <clip-path
+        android:pathData="M0,0h20v20h-20z"/>
+    <path
+        android:pathData="M11.5,16.833V15.271C12.694,14.951 13.66,14.306 14.396,13.333C15.132,12.347 15.5,11.236 15.5,10C15.5,8.764 15.125,7.667 14.375,6.708C13.639,5.736 12.681,5.069 11.5,4.708V3.146C13.111,3.493 14.431,4.306 15.458,5.583C16.486,6.861 17,8.326 17,9.979C17,11.632 16.486,13.104 15.458,14.396C14.444,15.674 13.125,16.486 11.5,16.833ZM3,11.979V7.979H6L10,3.979V15.979L6,11.979H3ZM11.5,13.125V6.833C12.125,7.111 12.611,7.535 12.958,8.104C13.319,8.674 13.5,9.299 13.5,9.979C13.5,10.66 13.319,11.285 12.958,11.854C12.611,12.41 12.125,12.833 11.5,13.125Z"
+        android:fillColor="#ECDFE5"/>
+  </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_volume_ringer_vibrate.xml b/packages/SystemUI/res/drawable/ic_volume_ringer_vibrate.xml
index b18e0a7..2cbbb0d 100644
--- a/packages/SystemUI/res/drawable/ic_volume_ringer_vibrate.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_ringer_vibrate.xml
@@ -1,27 +1,28 @@
 <!--
-     Copyright (C) 2017 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.
--->
+  ~ Copyright (C) 2025 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.
+  -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:height="19dp"
-    android:width="19dp"
-    android:viewportHeight="24.0"
-    android:viewportWidth="24.0"
-    android:tint="?android:attr/textColorPrimary">
-
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+  <group>
+    <clip-path
+        android:pathData="M0,0h20v20h-20z"/>
     <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M1,9h2v6H1V9zM4,17h2V7H4V17zM21,9v6h2V9H21zM18,17h2V7h-2V17zM17,5.5v13c0,0.83 -0.67,1.5 -1.5,1.5h-7C7.67,20 7,19.33 7,18.5v-13C7,4.67 7.67,4 8.5,4h7C16.33,4 17,4.67 17,5.5zM15,6H9v12h6V6z"/>
-
+        android:pathData="M0,12.5V7.5H1.5V12.5H0ZM2.5,14V6H4V14H2.5ZM18.5,12.5V7.5H20V12.5H18.5ZM16,14V6H17.5V14H16ZM6.5,17C6.083,17 5.729,16.854 5.438,16.563C5.146,16.271 5,15.917 5,15.5V4.5C5,4.083 5.146,3.729 5.438,3.438C5.729,3.146 6.083,3 6.5,3H13.5C13.917,3 14.271,3.146 14.563,3.438C14.854,3.729 15,4.083 15,4.5V15.5C15,15.917 14.854,16.271 14.563,16.563C14.271,16.854 13.917,17 13.5,17H6.5Z"
+        android:fillColor="#ECDFE5"/>
+  </group>
 </vector>
diff --git a/packages/SystemUI/res/drawable/mobile_network_type_background_updated.xml b/packages/SystemUI/res/drawable/mobile_network_type_background_updated.xml
new file mode 100644
index 0000000..7b55b3c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/mobile_network_type_background_updated.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  ~
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle"
+    >
+    <corners
+        android:topLeftRadius="0dp"
+        android:topRightRadius="@dimen/status_bar_mobile_container_corner_radius_updated"
+        android:bottomRightRadius="0dp"
+        android:bottomLeftRadius="@dimen/status_bar_mobile_container_corner_radius_updated"/>
+    <solid android:color="#FFF" />
+    <padding
+        android:left="2sp"
+        android:right="2sp"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
index b83f15a..c0cb5ef 100644
--- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
+++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
@@ -14,4 +14,4 @@
     limitations under the License.
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:drawable="@drawable/ic_speaker_mute" />
+    android:drawable="@drawable/ic_legacy_speaker_mute" />
diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_vibrate.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_vibrate.xml
index 21a4c17..4d68d67 100644
--- a/packages/SystemUI/res/drawable/stat_sys_ringer_vibrate.xml
+++ b/packages/SystemUI/res/drawable/stat_sys_ringer_vibrate.xml
@@ -16,4 +16,4 @@
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
     android:insetLeft="2.5dp"
     android:insetRight="2.5dp"
-    android:drawable="@drawable/ic_volume_ringer_vibrate" />
\ No newline at end of file
+    android:drawable="@drawable/ic_legacy_volume_ringer_vibrate" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index 5922a7d..67e9701 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -131,7 +131,7 @@
                     style="@style/InternetDialog.Network">
 
                     <FrameLayout
-                        android:layout_width="24dp"
+                        android:layout_width="wrap_content"
                         android:layout_height="24dp"
                         android:clickable="false"
                         android:layout_gravity="center_vertical|start">
diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml
index 8fd10fb..8c34cd4 100644
--- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml
+++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml
@@ -29,7 +29,6 @@
         android:layout_height="wrap_content"
         android:singleLine="true"
         android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
-        android:textSize="@*android:dimen/notification_2025_title_text_size"
         android:paddingEnd="4dp"
     />
     <TextView
diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
index 35f2ef9..a338e4c 100644
--- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
+++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
@@ -54,7 +54,6 @@
         android:singleLine="true"
         android:paddingEnd="4dp"
         android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
-        android:textSize="@*android:dimen/notification_2025_title_text_size"
     />
 
     <TextView
diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer_legacy.xml b/packages/SystemUI/res/layout/volume_ringer_drawer_legacy.xml
index 0efbc6d..c7e0f7d 100644
--- a/packages/SystemUI/res/layout/volume_ringer_drawer_legacy.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_drawer_legacy.xml
@@ -67,7 +67,7 @@
                     android:layout_width="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_height="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_gravity="center"
-                    android:src="@drawable/ic_volume_ringer_vibrate"
+                    android:src="@drawable/ic_legacy_volume_ringer_vibrate"
                     android:tint="?android:attr/textColorPrimary" />
 
             </FrameLayout>
@@ -85,7 +85,7 @@
                     android:layout_width="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_height="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_gravity="center"
-                    android:src="@drawable/ic_speaker_mute"
+                    android:src="@drawable/ic_legacy_speaker_mute"
                     android:tint="?android:attr/textColorPrimary" />
 
             </FrameLayout>
@@ -103,7 +103,7 @@
                     android:layout_width="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_height="@dimen/volume_ringer_drawer_icon_size"
                     android:layout_gravity="center"
-                    android:src="@drawable/ic_speaker_on"
+                    android:src="@drawable/ic_legacy_speaker_on"
                     android:tint="?android:attr/textColorPrimary" />
 
             </FrameLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 09aa224..8d10e39 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -119,7 +119,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes
+        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects
     </string>
 
     <!-- The tiles to display in QuickSettings -->
@@ -175,6 +175,9 @@
     <!-- Minimum display time for a heads up notification if throttling is enabled, in milliseconds. -->
     <integer name="heads_up_notification_minimum_time_with_throttling">500</integer>
 
+    <!-- Minimum display time for a heads up notification that was shown from a user action (like tapping on a different part of the UI), in milliseconds. -->
+    <integer name="heads_up_notification_minimum_time_for_user_initiated">3000</integer>
+
     <!-- Display time for a sticky heads up notification, in milliseconds. -->
     <integer name="sticky_heads_up_notification_time">60000</integer>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2bac87d..7d0c393 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -203,16 +203,32 @@
     <!-- Size of the view displaying the mobile signal icon in the status bar. This value should
         match the core/status_bar_system_icon_size and change to sp unit -->
     <dimen name="status_bar_mobile_signal_size">15sp</dimen>
-    <dimen name="status_bar_mobile_signal_size_updated">14sp</dimen>
+    <dimen name="status_bar_mobile_signal_size_updated">12sp</dimen>
+
     <!-- Size of the view displaying the mobile signal icon in the status bar. This value should
-        match the viewport height of mobile signal drawables such as ic_lte_mobiledata -->
+        match the viewport height of mobile signal drawables such as ic_lte_mobiledata
+        Note: can be removed once new_status_bar_icons is rolled out -->
     <dimen name="status_bar_mobile_type_size">16sp</dimen>
     <!-- Size of the view that contains the network type. Should be equal to
-    status_bar_mobile_type_size + 2, to account for 1sp top and bottom padding -->
+        status_bar_mobile_type_size + 2, to account for 1sp top and bottom padding
+        Note: can be removed once new_status_bar_icons is rolled out -->
     <dimen name="status_bar_mobile_container_height">18sp</dimen>
     <!-- Corner radius for the background of the network type indicator. Should be equal to
-        status_bar_mobile_container_height / 2 -->
+        status_bar_mobile_container_height / 2
+        Note: can be removed once new_status_bar_icons is rolled out -->
     <dimen name="status_bar_mobile_container_corner_radius">9sp</dimen>
+
+    <!-- Size of the view displaying the mobile signal icon in the status bar. This value should
+        match the viewport height of mobile signal drawables such as ic_lte_mobiledata -->
+    <dimen name="status_bar_mobile_type_size_updated">12sp</dimen>
+    <!-- Size of the view that contains the network type. Should be equal to
+   status_bar_mobile_type_size + 2, to account for 1sp top and bottom padding -->
+    <dimen name="status_bar_mobile_container_height_updated">14sp</dimen>
+    <dimen name="status_bar_mobile_container_margin_end">2sp</dimen>
+    <!-- Corner radius for the background of the network type indicator. Should be equal to
+        status_bar_mobile_container_height / 2 -->
+    <dimen name="status_bar_mobile_container_corner_radius_updated">7sp</dimen>
+
     <!-- Size of the view displaying the mobile roam icon in the status bar. This value should
         match the viewport size of drawable stat_sys_roaming -->
     <dimen name="status_bar_mobile_roam_size">8sp</dimen>
@@ -1806,7 +1822,8 @@
     <!-- The activity chip side padding, used with the default phone icon. -->
     <dimen name="ongoing_activity_chip_side_padding">12dp</dimen>
     <!-- The activity chip side padding, used with an icon that has embedded padding (e.g. if the icon comes from the notification's smallIcon field). If the icon has padding, the chip itself can have less padding. -->
-    <dimen name="ongoing_activity_chip_side_padding_for_embedded_padding_icon">6dp</dimen>
+    <dimen name="ongoing_activity_chip_side_padding_for_embedded_padding_icon">2dp</dimen>
+    <dimen name="ongoing_activity_chip_side_padding_for_embedded_padding_icon_legacy">6dp</dimen>
     <!-- The icon size, used with the default phone icon. -->
     <dimen name="ongoing_activity_chip_icon_size">16dp</dimen>
     <!-- The icon size, used with an icon that has embedded padding. (If the icon has embedded padding, we need to make the whole icon larger so the icon itself doesn't look small.) -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e077b41..c0eea15 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2341,7 +2341,9 @@
     <!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] -->
     <string name="system_multitasking_lhs">Use split screen with app on the left</string>
     <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
-    <string name="system_multitasking_full_screen">Switch to full screen</string>
+    <string name="system_multitasking_full_screen">Use full screen</string>
+    <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] -->
+    <string name="system_multitasking_desktop_view">Use desktop view</string>
     <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
     <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
     <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index d885e00..faf06f3 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -358,4 +358,14 @@
         <item>Off</item>
         <item>On</item>
     </string-array>
+
+    <!-- State names for desktop effects tile: unavailable, off, on.
+     This subtitle is shown when the tile is in that particular state but does not set its own
+     subtitle, so some of these may never appear on screen. They should still be translated as
+     if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_desktopeffects">
+        <item>Unavailable</item>
+        <item>Off</item>
+        <item>On</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index f528ec8..860a496 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -20,22 +20,16 @@
 import android.content.res.Configuration;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.SystemClock;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
 import android.util.Log;
 import android.util.Pair;
 import android.view.View;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.util.ViewController;
 
-import java.lang.ref.WeakReference;
-
 import javax.inject.Inject;
 
 /**
@@ -54,39 +48,8 @@
     private Pair<BiometricSourceType, Long> mMessageBiometricSource = null;
     private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L;
 
-    /**
-     * Delay before speaking an accessibility announcement. Used to prevent
-     * lift-to-type from interrupting itself.
-     */
-    private static final long ANNOUNCEMENT_DELAY = 250;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final ConfigurationController mConfigurationController;
-    private final AnnounceRunnable mAnnounceRunnable;
-    private final TextWatcher mTextWatcher = new TextWatcher() {
-        @Override
-        public void afterTextChanged(Editable editable) {
-            CharSequence msg = editable;
-            if (!TextUtils.isEmpty(msg)) {
-                mView.removeCallbacks(mAnnounceRunnable);
-                mAnnounceRunnable.setTextToAnnounce(msg);
-                mView.postDelayed(() -> {
-                    if (msg == mView.getText()) {
-                        mAnnounceRunnable.run();
-                    }
-                }, ANNOUNCEMENT_DELAY);
-            }
-        }
-
-        @Override
-        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-            /* no-op */
-        }
-
-        @Override
-        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-            /* no-op */
-        }
-    };
 
     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
         public void onFinishedGoingToSleep(int why) {
@@ -122,7 +85,6 @@
 
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mConfigurationController = configurationController;
-        mAnnounceRunnable = new AnnounceRunnable(mView);
     }
 
     @Override
@@ -131,14 +93,12 @@
         mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
         mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive());
         mView.onThemeChanged();
-        mView.addTextChangedListener(mTextWatcher);
     }
 
     @Override
     protected void onViewDetached() {
         mConfigurationController.removeCallback(mConfigurationListener);
         mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
-        mView.removeTextChangedListener(mTextWatcher);
     }
 
     /**
@@ -232,30 +192,4 @@
                     view, mKeyguardUpdateMonitor, mConfigurationController);
         }
     }
-
-    /**
-     * Runnable used to delay accessibility announcements.
-     */
-    @VisibleForTesting
-    public static class AnnounceRunnable implements Runnable {
-        private final WeakReference<View> mHost;
-        private CharSequence mTextToAnnounce;
-
-        AnnounceRunnable(View host) {
-            mHost = new WeakReference<>(host);
-        }
-
-        /** Sets the text to announce. */
-        public void setTextToAnnounce(CharSequence textToAnnounce) {
-            mTextToAnnounce = textToAnnounce;
-        }
-
-        @Override
-        public void run() {
-            final View host = mHost.get();
-            if (host != null && host.isVisibleToUser()) {
-                host.announceForAccessibility(mTextToAnnounce);
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 5d63c2a..4a4cb7a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -43,6 +43,8 @@
 import com.android.settingslib.animation.AppearAnimationCreator;
 import com.android.settingslib.animation.AppearAnimationUtils;
 import com.android.settingslib.animation.DisappearAnimationUtils;
+import com.android.systemui.Flags;
+import com.android.systemui.bouncer.shared.constants.PatternBouncerConstants.ColorId;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
 
@@ -227,6 +229,18 @@
         super.onFinishInflate();
 
         mLockPatternView = findViewById(R.id.lockPatternView);
+        if (Flags.bouncerUiRevamp2()) {
+            mLockPatternView.setDotColors(mContext.getColor(ColorId.dotColor), mContext.getColor(
+                    ColorId.activatedDotColor));
+            mLockPatternView.setColors(mContext.getColor(ColorId.pathColor), 0, 0);
+            mLockPatternView.setDotSizes(
+                    getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_dot_size),
+                    getResources().getDimensionPixelSize(
+                            R.dimen.keyguard_pattern_activated_dot_size));
+            mLockPatternView.setPathWidth(
+                    getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width));
+            mLockPatternView.setKeepDotActivated(true);
+        }
 
         mEcaView = findViewById(R.id.keyguard_selector_fade_container);
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index 245283d..04d4c2a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -184,7 +184,9 @@
         }
         mDeleteButton = findViewById(R.id.delete_button);
         if (Flags.bouncerUiRevamp2()) {
-            mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete);
+            mDeleteButton.setDrawableForTransparentMode(R.drawable.pin_bouncer_delete_filled);
+            mDeleteButton.setDefaultDrawable(R.drawable.pin_bouncer_delete_outline);
+            mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete_outline);
         }
         mDeleteButton.setVisibility(View.VISIBLE);
 
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
index 0ff9323..584ebb50 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
@@ -25,6 +25,7 @@
 import android.view.MotionEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 
 import com.android.systemui.Flags;
@@ -42,6 +43,12 @@
     private int mStyleAttr;
     private boolean mIsTransparentMode;
 
+    @DrawableRes
+    private int mDrawableForTransparentMode = 0;
+
+    @DrawableRes
+    private int mDefaultDrawable = 0;
+
     public NumPadButton(Context context, AttributeSet attrs) {
         super(context, attrs);
         mStyleAttr = attrs.getStyleAttribute();
@@ -123,8 +130,14 @@
         mIsTransparentMode = isTransparentMode;
 
         if (isTransparentMode) {
+            if (mDrawableForTransparentMode != 0) {
+                setImageResource(mDrawableForTransparentMode);
+            }
             setBackgroundColor(getResources().getColor(android.R.color.transparent));
         } else {
+            if (mDefaultDrawable != 0) {
+                setImageResource(mDefaultDrawable);
+            }
             Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background);
             if (Flags.bouncerUiRevamp2() && bgDrawable != null) {
                 bgDrawable.setTint(Color.actionBg);
@@ -154,4 +167,19 @@
         super.onInitializeAccessibilityNodeInfo(info);
         info.setTextEntryKey(true);
     }
+
+    /**
+     * Drawable to use when transparent mode is enabled
+     */
+    public void setDrawableForTransparentMode(@DrawableRes int drawableResId) {
+        mDrawableForTransparentMode = drawableResId;
+    }
+
+    /**
+     * Drawable to use when transparent mode is not enabled.
+     */
+    public void setDefaultDrawable(@DrawableRes int drawableResId) {
+        mDefaultDrawable = drawableResId;
+        setImageResource(mDefaultDrawable);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 438184d..22ecb0a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -30,7 +30,6 @@
 import android.content.res.Resources;
 import android.media.AudioManager;
 import android.os.Bundle;
-import android.os.Handler;
 import android.provider.Settings;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -49,6 +48,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -57,7 +57,6 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
-import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory;
@@ -67,6 +66,7 @@
 import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory;
 import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
 import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.res.R;
@@ -79,6 +79,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
 /**
@@ -101,7 +102,8 @@
     private final DialogTransitionAnimator mDialogTransitionAnimator;
     private final ActivityStarter mActivityStarter;
     private final LocalBluetoothManager mLocalBluetoothManager;
-    private final Handler mMainHandler;
+    private final Executor mMainExecutor;
+    private final Executor mBgExecutor;
     private final AudioManager mAudioManager;
     private final LocalBluetoothProfileManager mProfileManager;
     private final HearingDevicesUiEventLogger mUiEventLogger;
@@ -109,8 +111,6 @@
     private final int mLaunchSourceId;
 
     private SystemUIDialog mDialog;
-
-    private List<DeviceItem> mHearingDeviceItemList;
     private HearingDevicesListAdapter mDeviceListAdapter;
 
     private View mPresetLayout;
@@ -122,14 +122,14 @@
                 @Override
                 public void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos,
                         int activePresetIndex) {
-                    mMainHandler.post(
+                    mMainExecutor.execute(
                             () -> refreshPresetUi(presetInfos, activePresetIndex));
                 }
 
                 @Override
                 public void onPresetCommandFailed(int reason) {
                     mPresetController.refreshPresetInfo();
-                    mMainHandler.post(() -> {
+                    mMainExecutor.execute(() -> {
                         showErrorToast(R.string.hearing_devices_presets_error);
                     });
                 }
@@ -166,7 +166,8 @@
             ActivityStarter activityStarter,
             DialogTransitionAnimator dialogTransitionAnimator,
             @Nullable LocalBluetoothManager localBluetoothManager,
-            @Main Handler handler,
+            @Main Executor mainExecutor,
+            @Background Executor bgExecutor,
             AudioManager audioManager,
             HearingDevicesUiEventLogger uiEventLogger) {
         mShowPairNewDevice = showPairNewDevice;
@@ -174,7 +175,8 @@
         mActivityStarter = activityStarter;
         mDialogTransitionAnimator = dialogTransitionAnimator;
         mLocalBluetoothManager = localBluetoothManager;
-        mMainHandler = handler;
+        mMainExecutor = mainExecutor;
+        mBgExecutor = bgExecutor;
         mAudioManager = audioManager;
         mProfileManager = localBluetoothManager.getProfileManager();
         mUiEventLogger = uiEventLogger;
@@ -227,9 +229,10 @@
     @Override
     public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
             int bluetoothProfile) {
-        refreshDeviceUi();
-        mMainHandler.post(() -> {
-            CachedBluetoothDevice device = getActiveHearingDevice();
+        List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList();
+        refreshDeviceUi(hearingDeviceItemList);
+        mMainExecutor.execute(() -> {
+            CachedBluetoothDevice device = getActiveHearingDevice(hearingDeviceItemList);
             if (mPresetController != null) {
                 mPresetController.setDevice(device);
                 mPresetLayout.setVisibility(
@@ -244,13 +247,15 @@
     @Override
     public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
             int state, int bluetoothProfile) {
-        refreshDeviceUi();
+        List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList();
+        refreshDeviceUi(hearingDeviceItemList);
     }
 
     @Override
     public void onAclConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
             int state) {
-        refreshDeviceUi();
+        List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList();
+        refreshDeviceUi(hearingDeviceItemList);
     }
 
     @Override
@@ -280,18 +285,25 @@
 
         mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId);
 
-        setupDeviceListView(dialog);
-        setupPairNewDeviceButton(dialog);
-        setupPresetSpinner(dialog);
-        if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) {
-            setupAmbientControls();
-        }
-        setupRelatedToolsView(dialog);
+        mBgExecutor.execute(() -> {
+            List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList();
+            CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
+                    hearingDeviceItemList);
+            mMainExecutor.execute(() -> {
+                setupDeviceListView(dialog, hearingDeviceItemList);
+                setupPairNewDeviceButton(dialog);
+                setupPresetSpinner(dialog, activeHearingDevice);
+                if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) {
+                    setupAmbientControls(activeHearingDevice);
+                }
+                setupRelatedToolsView(dialog);
+            });
+        });
     }
 
     @Override
     public void onStart(@NonNull SystemUIDialog dialog) {
-        ThreadUtils.postOnBackgroundThread(() -> {
+        mBgExecutor.execute(() -> {
             if (mLocalBluetoothManager != null) {
                 mLocalBluetoothManager.getEventManager().registerCallback(this);
             }
@@ -306,7 +318,7 @@
 
     @Override
     public void onStop(@NonNull SystemUIDialog dialog) {
-        ThreadUtils.postOnBackgroundThread(() -> {
+        mBgExecutor.execute(() -> {
             if (mLocalBluetoothManager != null) {
                 mLocalBluetoothManager.getEventManager().unregisterCallback(this);
             }
@@ -319,17 +331,18 @@
         });
     }
 
-    private void setupDeviceListView(SystemUIDialog dialog) {
+    private void setupDeviceListView(SystemUIDialog dialog,
+            List<DeviceItem> hearingDeviceItemList) {
         final RecyclerView deviceList = dialog.requireViewById(R.id.device_list);
         deviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
-        mHearingDeviceItemList = getHearingDeviceItemList();
-        mDeviceListAdapter = new HearingDevicesListAdapter(mHearingDeviceItemList, this);
+        mDeviceListAdapter = new HearingDevicesListAdapter(hearingDeviceItemList, this);
         deviceList.setAdapter(mDeviceListAdapter);
     }
 
-    private void setupPresetSpinner(SystemUIDialog dialog) {
+    private void setupPresetSpinner(SystemUIDialog dialog,
+            CachedBluetoothDevice activeHearingDevice) {
         mPresetController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback);
-        mPresetController.setDevice(getActiveHearingDevice());
+        mPresetController.setDevice(activeHearingDevice);
 
         mPresetSpinner = dialog.requireViewById(R.id.preset_spinner);
         mPresetInfoAdapter = new HearingDevicesSpinnerAdapter(dialog.getContext());
@@ -367,12 +380,12 @@
         mPresetLayout.setVisibility(mPresetController.isPresetControlAvailable() ? VISIBLE : GONE);
     }
 
-    private void setupAmbientControls() {
+    private void setupAmbientControls(CachedBluetoothDevice activeHearingDevice) {
         final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout);
         mAmbientController = new AmbientVolumeUiController(
                 mDialog.getContext(), mLocalBluetoothManager, ambientLayout);
         mAmbientController.setShowUiWhenLocalDataExist(false);
-        mAmbientController.loadDevice(getActiveHearingDevice());
+        mAmbientController.loadDevice(activeHearingDevice);
     }
 
     private void setupPairNewDeviceButton(SystemUIDialog dialog) {
@@ -429,10 +442,11 @@
         }
     }
 
-    private void refreshDeviceUi() {
-        mHearingDeviceItemList = getHearingDeviceItemList();
-        mMainHandler.post(() -> {
-            mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList);
+    private void refreshDeviceUi(List<DeviceItem> hearingDeviceItemList) {
+        mMainExecutor.execute(() -> {
+            if (mDeviceListAdapter != null) {
+                mDeviceListAdapter.refreshDeviceItemList(hearingDeviceItemList);
+            }
         });
     }
 
@@ -463,21 +477,27 @@
     }
 
     @Nullable
-    private CachedBluetoothDevice getActiveHearingDevice() {
-        return mHearingDeviceItemList.stream()
+    private static CachedBluetoothDevice getActiveHearingDevice(
+            List<DeviceItem> hearingDeviceItemList) {
+        return hearingDeviceItemList.stream()
                 .filter(item -> item.getType() == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
                 .map(DeviceItem::getCachedBluetoothDevice)
                 .findFirst()
                 .orElse(null);
     }
 
+    @WorkerThread
     private DeviceItem createHearingDeviceItem(CachedBluetoothDevice cachedDevice) {
         final Context context = mDialog.getContext();
         if (cachedDevice == null) {
             return null;
         }
+        int mode = mAudioManager.getMode();
+        boolean isOngoingCall = mode == AudioManager.MODE_RINGTONE
+                || mode == AudioManager.MODE_IN_CALL
+                || mode == AudioManager.MODE_IN_COMMUNICATION;
         for (DeviceItemFactory itemFactory : mHearingDeviceItemFactoryList) {
-            if (itemFactory.isFilterMatched(context, cachedDevice, mAudioManager)) {
+            if (itemFactory.isFilterMatched(context, cachedDevice, isOngoingCall)) {
                 return itemFactory.create(context, cachedDevice);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt
new file mode 100644
index 0000000..2d21d65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 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.activity.data.model
+
+/** Describes an app's previous and current visibility to the user. */
+data class AppVisibilityModel(
+    /** True if the app is currently visible to the user and false otherwise. */
+    val isAppCurrentlyVisible: Boolean = false,
+    /**
+     * The last time this app became visible to the user, in
+     * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't
+     * become visible since the flow started collection.
+     */
+    val lastAppVisibleTime: Long? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
index 94614b7..11831ea 100644
--- a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
@@ -18,9 +18,11 @@
 
 import android.app.ActivityManager
 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import com.android.systemui.activity.data.model.AppVisibilityModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.core.Logger
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
@@ -29,9 +31,23 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
 
 /** Repository for interfacing with [ActivityManager]. */
 interface ActivityManagerRepository {
+
+    /**
+     * Given a UID, creates a flow that emits details about when the process with the given UID was
+     * and is visible to the user.
+     *
+     * @param identifyingLogTag a tag identifying who created this flow, used for logging.
+     */
+    fun createAppVisibilityFlow(
+        creationUid: Int,
+        logger: Logger,
+        identifyingLogTag: String,
+    ): Flow<AppVisibilityModel>
+
     /**
      * Given a UID, creates a flow that emits true when the process with the given UID is visible to
      * the user and false otherwise.
@@ -50,8 +66,38 @@
 @Inject
 constructor(
     @Background private val backgroundContext: CoroutineContext,
+    private val systemClock: SystemClock,
     private val activityManager: ActivityManager,
 ) : ActivityManagerRepository {
+
+    override fun createAppVisibilityFlow(
+        creationUid: Int,
+        logger: Logger,
+        identifyingLogTag: String,
+    ): Flow<AppVisibilityModel> {
+        return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag)
+            .distinctUntilChanged()
+            .scan(initial = AppVisibilityModel()) {
+                oldState: AppVisibilityModel,
+                newIsVisible: Boolean ->
+                if (newIsVisible) {
+                    val lastAppVisibleTime = systemClock.currentTimeMillis()
+                    logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) {
+                        str1 = identifyingLogTag
+                        long1 = lastAppVisibleTime
+                    }
+                    AppVisibilityModel(
+                        isAppCurrentlyVisible = true,
+                        lastAppVisibleTime = lastAppVisibleTime,
+                    )
+                } else {
+                    // Reset the current status while maintaining the lastAppVisibleTime
+                    oldState.copy(isAppCurrentlyVisible = false)
+                }
+            }
+            .distinctUntilChanged()
+    }
+
     override fun createIsAppVisibleFlow(
         creationUid: Int,
         logger: Logger,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt
index ff2d9ef..1c9cf8d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -71,6 +72,7 @@
     private val bluetoothStateInteractor: BluetoothStateInteractor,
     private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
     private val audioSharingInteractor: AudioSharingInteractor,
+    private val audioModeInteractor: AudioModeInteractor,
     private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory,
     private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
@@ -167,6 +169,7 @@
                 // the device item list and animate the progress bar.
                 merge(
                         deviceItemInteractor.deviceItemUpdateRequest,
+                        audioModeInteractor.isOngoingCall,
                         bluetoothDeviceMetadataInteractor.metadataUpdate,
                         if (
                             audioSharingInteractor.audioSharingAvailable() &&
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 095e6e7..bfbc27d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothDevice
 import android.content.Context
-import android.media.AudioManager
 import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -43,7 +42,7 @@
     abstract fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean
 
@@ -51,8 +50,8 @@
     fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
-    ): Boolean = isFilterMatched(context, cachedDevice, audioManager, false)
+        isOngoingCall: Boolean,
+    ): Boolean = isFilterMatched(context, cachedDevice, isOngoingCall, false)
 
     abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem
 
@@ -88,11 +87,11 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
-            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, isOngoingCall)
     }
 
     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
@@ -113,10 +112,11 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return audioSharingAvailable &&
+            !isOngoingCall &&
             BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager)
     }
 
@@ -140,11 +140,12 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return audioSharingAvailable &&
-            super.isFilterMatched(context, cachedDevice, audioManager, true) &&
+            !isOngoingCall &&
+            super.isFilterMatched(context, cachedDevice, false, true) &&
             BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
                 cachedDevice,
                 localBluetoothManager,
@@ -170,7 +171,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
@@ -182,11 +183,11 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
-            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, isOngoingCall)
     }
 
     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
@@ -206,7 +207,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
@@ -218,14 +219,14 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
-                BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
+                BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, isOngoingCall)
         } else {
-            BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
+            BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, isOngoingCall)
         }
     }
 
@@ -246,7 +247,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
@@ -275,7 +276,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager,
+        isOngoingCall: Boolean,
         audioSharingAvailable: Boolean,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 1e0ba8e..b606c19 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -19,10 +19,10 @@
 import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothDevice
 import android.content.Context
-import android.media.AudioManager
 import com.android.settingslib.bluetooth.BluetoothCallback
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -39,6 +39,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.isActive
@@ -51,7 +52,6 @@
 constructor(
     private val bluetoothTileDialogRepository: BluetoothTileDialogRepository,
     private val audioSharingInteractor: AudioSharingInteractor,
-    private val audioManager: AudioManager,
     private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
     private val localBluetoothManager: LocalBluetoothManager?,
     private val systemClock: SystemClock,
@@ -60,6 +60,7 @@
     private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>,
     @Application private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val audioModeInteractor: AudioModeInteractor,
 ) {
 
     private val mutableDeviceItemUpdate: MutableSharedFlow<List<DeviceItem>> =
@@ -118,8 +119,12 @@
 
     internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) {
         withContext(backgroundDispatcher) {
+            if (!isActive) {
+                return@withContext
+            }
             val start = systemClock.elapsedRealtime()
             val audioSharingAvailable = audioSharingInteractor.audioSharingAvailable()
+            val isOngoingCall = audioModeInteractor.isOngoingCall.first()
             val deviceItems =
                 bluetoothTileDialogRepository.cachedDevices
                     .mapNotNull { cachedDevice ->
@@ -128,7 +133,7 @@
                                 it.isFilterMatched(
                                     context,
                                     cachedDevice,
-                                    audioManager,
+                                    isOngoingCall,
                                     audioSharingAvailable,
                                 )
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
index 149efcd..3ef50f6 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
@@ -87,6 +87,14 @@
     }
 }
 
+object PatternBouncerConstants {
+    object ColorId {
+        @JvmField val dotColor = colors.materialColorOnSurfaceVariant
+        @JvmField val activatedDotColor = colors.materialColorOnPrimary
+        @JvmField val pathColor = colors.materialColorPrimary
+    }
+}
+
 object PinBouncerConstants {
     @JvmField
     val pinShapes = c(old = R.array.bouncer_pin_shapes, new = R.array.updated_bouncer_pin_shapes)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
index 434a9ce..7d8945a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -191,7 +191,6 @@
                             .filter { it == EXPANSION_VISIBLE }
                             .collect {
                                 securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
-                                view.announceForAccessibility(securityContainerController.title)
                             }
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 0a9bd42..bf4445b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -151,5 +151,7 @@
         override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
 
         override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
+
+        override fun freezeAndAnimateToCurrentState() = Unit
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
index ca49de3..a84c457 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
@@ -31,4 +31,6 @@
     val ToEditMode = TransitionKey("ToEditMode")
     /** Transition to the glanceable hub after exiting edit mode */
     val FromEditMode = TransitionKey("FromEditMode")
+    /** Swipes the glanceable hub in/out of view */
+    val Swipe = TransitionKey("Swipe")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 2169881..cce1ae1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -359,7 +359,13 @@
     /** See [CommunalSettingsInteractor.isV2FlagEnabled] */
     fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled()
 
-    fun swipeToHubEnabled(): Boolean = swipeToHub
+    val swipeToHubEnabled: StateFlow<Boolean> by lazy {
+        if (v2FlagEnabled()) {
+            communalInteractor.shouldShowCommunal
+        } else {
+            MutableStateFlow(swipeToHub)
+        }
+    }
 
     companion object {
         const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 8bff090..3c68e3a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -74,7 +74,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
 import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule;
-import com.android.systemui.statusbar.notification.dagger.NotificationStackModule;
 import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpModule;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -170,7 +169,6 @@
         WallpaperModule.class,
         ShortcutHelperModule.class,
         ContextualEducationModule.class,
-        NotificationStackModule.class,
 })
 public abstract class ReferenceSystemUIModule {
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index ebe228d..2650159 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -45,7 +45,6 @@
         // Internal notification backend dependencies
         crossAppPoliteNotifications dependsOn politeNotifications
         vibrateWhileUnlockedToken dependsOn politeNotifications
-        modesUi dependsOn modesApi
 
         // Internal notification frontend dependencies
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
@@ -71,9 +70,6 @@
     private inline val modesUi
         get() = FlagToken(android.app.Flags.FLAG_MODES_UI, android.app.Flags.modesUi())
 
-    private inline val modesApi
-        get() = FlagToken(android.app.Flags.FLAG_MODES_API, android.app.Flags.modesApi())
-
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
index b712fde..0d57a57 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
@@ -102,10 +102,9 @@
     }
 
     // This flow is used by the notification updater once an initial notification is launched. It
-    // listens to the device connection changes for both keyboard and touchpad. When either of the
-    // device is disconnected, resolve the tutorial type base on the latest connection state.
-    // Dropping the initial state because it's the existing notification. Filtering out BOTH because
-    // we only care about disconnections.
+    // listens to the changes of keyboard and touchpad connection and resolve the tutorial type base
+    // on the latest connection state.
+    // Dropping the initial state because it represents the existing notification.
     val tutorialTypeUpdates: Flow<TutorialType> =
         keyboardRepository.isAnyKeyboardConnected
             .combine(touchpadRepository.isAnyTouchpadConnected, ::Pair)
@@ -118,7 +117,6 @@
                 }
             }
             .drop(1)
-            .filter { it != TutorialType.BOTH }
 
     private suspend fun waitForDeviceConnection(deviceType: DeviceType) =
         isAnyDeviceConnected[deviceType]!!.filter { it }.first()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
index 464201f..b787fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.res.Resources
 import android.view.KeyEvent.KEYCODE_D
+import android.view.KeyEvent.KEYCODE_DPAD_DOWN
 import android.view.KeyEvent.KEYCODE_DPAD_LEFT
 import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
 import android.view.KeyEvent.KEYCODE_DPAD_UP
@@ -73,6 +74,15 @@
                 command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_UP)
             }
         )
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+            //  Switch to desktop view
+            //   - Meta + Ctrl + Down arrow
+            add(
+                shortcutInfo(resources.getString(R.string.system_multitasking_desktop_view)) {
+                    command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_DOWN)
+                }
+            )
+        }
         if (enableMoveToNextDisplayShortcut()) {
             // Move a window to the next display:
             //  - Meta + Ctrl + D
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
index 80bdc65..f692292 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
@@ -25,11 +25,17 @@
 class LockscreenSceneTransitionRepository @Inject constructor() {
 
     /**
-     * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the
-     * next transition into the Lockscreen scene is started. It will be consumed exactly once and
-     * after that the state will be set back to [DEFAULT_STATE].
+     * This [KeyguardState] will indicate which sub-state within KTF should be navigated to next.
+     *
+     * This can be either starting a transition to the `Lockscreen` scene or cancelling a transition
+     * from the `Lockscreen` scene and returning back to it.
+     *
+     * A `null` value means that no explicit target state was set and therefore the [DEFAULT_STATE]
+     * should be used.
+     *
+     * Once consumed, this state should be reset to `null`.
      */
-    val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE)
+    val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null)
 
     companion object {
         val DEFAULT_STATE = KeyguardState.LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 0700ec6..6f5f662 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -159,7 +159,6 @@
                     val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
                     val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
                     val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value
-                    val canStartDreaming = dreamManager.canStartDreaming(false)
 
                     if (!deviceEntryInteractor.isLockscreenEnabled()) {
                         if (!SceneContainerFlag.isEnabled) {
@@ -192,13 +191,6 @@
                         if (!SceneContainerFlag.isEnabled) {
                             transitionToGlanceableHub()
                         }
-                    } else if (canStartDreaming) {
-                        // If we're waking up to dream, transition directly to dreaming without
-                        // showing the lockscreen.
-                        startTransitionTo(
-                            KeyguardState.DREAMING,
-                            ownerReason = "moving from doze to dream",
-                        )
                     } else {
                         startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 5f82102..1b70ff8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -119,7 +119,8 @@
         } else {
             val targetState =
                 if (idle.currentScene == Scenes.Lockscreen) {
-                    transitionInteractor.startedKeyguardTransitionStep.value.from
+                    repository.nextLockscreenTargetState.value
+                        ?: transitionInteractor.startedKeyguardTransitionStep.value.from
                 } else {
                     UNDEFINED
                 }
@@ -197,11 +198,11 @@
             TransitionInfo(
                 ownerName = this::class.java.simpleName,
                 from = UNDEFINED,
-                to = repository.nextLockscreenTargetState.value,
+                to = repository.nextLockscreenTargetState.value ?: DEFAULT_STATE,
                 animator = null,
                 modeOnCanceled = TransitionModeOnCanceled.RESET,
             )
-        repository.nextLockscreenTargetState.value = DEFAULT_STATE
+        repository.nextLockscreenTargetState.value = null
         startTransition(newTransition)
     }
 
@@ -215,7 +216,7 @@
                 animator = null,
                 modeOnCanceled = TransitionModeOnCanceled.RESET,
             )
-        repository.nextLockscreenTargetState.value = DEFAULT_STATE
+        repository.nextLockscreenTargetState.value = null
         startTransition(newTransition)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 8e38538..da87e38 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -370,6 +370,14 @@
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
                     if (wallpaperFocalAreaViewModel.hasFocalArea.value) {
                         launch {
+                            wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect {
+                                wallpaperFocalAreaBounds ->
+                                wallpaperFocalAreaViewModel.setFocalAreaBounds(
+                                    wallpaperFocalAreaBounds
+                                )
+                            }
+                        }
+                        launch {
                             wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds
                                 .filterNotNull()
                                 .collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
index 9018c58..e6a85c6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
@@ -39,6 +39,4 @@
         )
 
     val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
-    // Notifications should not be shown while transitioning to dream.
-    val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
index c3182bf..1466d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
@@ -143,6 +143,12 @@
          *   place immediately.
          */
         override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {}
+
+        /**
+         * Called whenever the current active media notification changes. Should only be used if
+         * [SceneContainerFlag] is disabled
+         */
+        override fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {}
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 1464849..59f98d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -1434,6 +1434,9 @@
          *   place immediately.
          */
         fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+
+        /** Called whenever the current active media notification changes */
+        fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {}
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 173b600..93c4baf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -87,6 +87,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY
 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN
 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -155,6 +156,7 @@
     private val mediaCarouselViewModel: MediaCarouselViewModel,
     private val mediaViewControllerFactory: Provider<MediaViewController>,
     private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val mediaControlChipInteractor: MediaControlChipInteractor,
 ) : Dumpable {
     /** The current width of the carousel */
     var currentCarouselWidth: Int = 0
@@ -957,6 +959,9 @@
                 }
         }
         mediaCarouselScrollHandler.onPlayersChanged()
+        mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+            MediaPlayerData.getFirstActiveMediaData()
+        )
         MediaPlayerData.updateVisibleMediaPlayers()
         // Automatically scroll to the active player if needed
         if (shouldScrollToKey) {
@@ -1015,6 +1020,9 @@
                             )
                             updatePageIndicator()
                             mediaCarouselScrollHandler.onPlayersChanged()
+                            mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+                                MediaPlayerData.getFirstActiveMediaData()
+                            )
                             mediaFrame.requiresRemeasuring = true
                             onUiExecutionEnd?.run()
                         }
@@ -1023,6 +1031,9 @@
                     updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
                     updatePageIndicator()
                     mediaCarouselScrollHandler.onPlayersChanged()
+                    mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+                        MediaPlayerData.getFirstActiveMediaData()
+                    )
                     mediaFrame.requiresRemeasuring = true
                     onUiExecutionEnd?.run()
                 }
@@ -1036,6 +1047,9 @@
                 }
                 updatePageIndicator()
                 mediaCarouselScrollHandler.onPlayersChanged()
+                mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+                    MediaPlayerData.getFirstActiveMediaData()
+                )
                 mediaFrame.requiresRemeasuring = true
                 onUiExecutionEnd?.run()
             }
@@ -1194,6 +1208,9 @@
             mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
             removed.onDestroy()
             mediaCarouselScrollHandler.onPlayersChanged()
+            mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+                MediaPlayerData.getFirstActiveMediaData()
+            )
             updatePageIndicator()
 
             if (dismissMediaData) {
@@ -1928,6 +1945,16 @@
 
     fun visiblePlayerKeys() = visibleMediaPlayers.values
 
+    /** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */
+    fun getFirstActiveMediaData(): MediaData? {
+        mediaPlayers.entries.forEach { entry ->
+            if (!entry.key.isSsMediaRec && entry.key.data.active) {
+                return entry.key.data
+            }
+        }
+        return null
+    }
+
     /** Returns the index of the first non-timeout media. */
     fun firstActiveMediaIndex(): Int {
         mediaPlayers.entries.forEachIndexed { index, e ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
index 709723f..6022b7b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
@@ -18,6 +18,7 @@
 
 import androidx.recyclerview.widget.ListUpdateCallback
 import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
+import kotlin.math.min
 
 /** A [ListUpdateCallback] to apply media events needed to reach the new state. */
 class MediaViewModelListUpdateCallback(
@@ -46,7 +47,7 @@
     }
 
     override fun onChanged(position: Int, count: Int, payload: Any?) {
-        for (i in position until position + count) {
+        for (i in position until min(position + count, new.size)) {
             onUpdated(new[i], position)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
deleted file mode 100644
index f5e6232..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * Copyright (C) 2020 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.media.dialog;
-
-import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
-import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
-import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
-
-import android.annotation.DrawableRes;
-import android.annotation.StringRes;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.TextView;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.core.widget.CompoundButtonCompat;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.media.flags.Flags;
-import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
-import com.android.settingslib.media.MediaDevice;
-import com.android.systemui.res.R;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * Adapter for media output dialog.
- */
-public class MediaOutputAdapter extends MediaOutputBaseAdapter {
-
-    private static final String TAG = "MediaOutputAdapter";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final float DEVICE_DISABLED_ALPHA = 0.5f;
-    private static final float DEVICE_ACTIVE_ALPHA = 1f;
-    protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
-    private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
-
-    public MediaOutputAdapter(MediaSwitchingController controller) {
-        super(controller);
-        setHasStableIds(true);
-    }
-
-    @Override
-    public void updateItems() {
-        mMediaItemList.clear();
-        mMediaItemList.addAll(mController.getMediaItemList());
-        if (mShouldGroupSelectedMediaItems) {
-            if (mController.getSelectedMediaDevice().size() == 1) {
-                // Don't group devices if initially there isn't more than one selected.
-                mShouldGroupSelectedMediaItems = false;
-            }
-        }
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
-            int viewType) {
-        super.onCreateViewHolder(viewGroup, viewType);
-        switch (viewType) {
-            case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
-                return new MediaGroupDividerViewHolder(mHolderView);
-            case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
-            case MediaItem.MediaItemType.TYPE_DEVICE:
-            default:
-                return new MediaDeviceViewHolder(mHolderView);
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
-        if (position >= mMediaItemList.size()) {
-            if (DEBUG) {
-                Log.d(TAG, "Incorrect position: " + position + " list size: "
-                        + mMediaItemList.size());
-            }
-            return;
-        }
-        MediaItem currentMediaItem = mMediaItemList.get(position);
-        switch (currentMediaItem.getMediaItemType()) {
-            case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
-                ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle());
-                break;
-            case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
-                ((MediaDeviceViewHolder) viewHolder).onBindPairNewDevice();
-                break;
-            case MediaItem.MediaItemType.TYPE_DEVICE:
-                ((MediaDeviceViewHolder) viewHolder).onBind(
-                        currentMediaItem,
-                        position);
-                break;
-            default:
-                Log.d(TAG, "Incorrect position: " + position);
-        }
-    }
-
-    @Override
-    public long getItemId(int position) {
-        if (position >= mMediaItemList.size()) {
-            Log.d(TAG, "Incorrect position for item id: " + position);
-            return position;
-        }
-        MediaItem currentMediaItem = mMediaItemList.get(position);
-        return currentMediaItem.getMediaDevice().isPresent()
-                ? currentMediaItem.getMediaDevice().get().getId().hashCode()
-                : position;
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        if (position >= mMediaItemList.size()) {
-            Log.d(TAG, "Incorrect position for item type: " + position);
-            return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER;
-        }
-        return mMediaItemList.get(position).getMediaItemType();
-    }
-
-    @Override
-    public int getItemCount() {
-        return mMediaItemList.size();
-    }
-
-    class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
-
-        MediaDeviceViewHolder(View view) {
-            super(view);
-        }
-
-        void onBind(MediaItem mediaItem, int position) {
-            MediaDevice device = mediaItem.getMediaDevice().get();
-            super.onBind(device, position);
-            boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
-            final boolean currentlyConnected = isCurrentlyConnected(device);
-            boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device);
-            boolean isDeselectable =
-                    isDeviceIncluded(mController.getDeselectableMediaDevice(), device);
-            boolean isSelectable = isDeviceIncluded(mController.getSelectableMediaDevice(), device);
-            boolean isTransferable =
-                    isDeviceIncluded(mController.getTransferableMediaDevices(), device);
-            boolean hasRouteListingPreferenceItem = device.hasRouteListingPreferenceItem();
-
-            if (DEBUG) {
-                Log.d(
-                        TAG,
-                        "["
-                                + position
-                                + "] "
-                                + device.getName()
-                                + " ["
-                                + (isDeselectable ? "deselectable" : "")
-                                + "] ["
-                                + (isSelected ? "selected" : "")
-                                + "] ["
-                                + (isSelectable ? "selectable" : "")
-                                + "] ["
-                                + (isTransferable ? "transferable" : "")
-                                + "] ["
-                                + (hasRouteListingPreferenceItem ? "hasListingPreference" : "")
-                                + "]");
-            }
-
-            boolean isDeviceGroup = false;
-            boolean hideGroupItem = false;
-            GroupStatus groupStatus = null;
-            OngoingSessionStatus ongoingSessionStatus = null;
-            ConnectionState connectionState = ConnectionState.DISCONNECTED;
-            boolean restrictVolumeAdjustment = mController.hasAdjustVolumeUserRestriction();
-            String subtitle = null;
-            Drawable deviceStatusIcon = null;
-            boolean deviceDisabled = false;
-            View.OnClickListener clickListener = null;
-
-            if (mCurrentActivePosition == position) {
-                mCurrentActivePosition = -1;
-            }
-            mItemLayout.setVisibility(View.VISIBLE);
-
-            if (mController.isAnyDeviceTransferring()) {
-                if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
-                    connectionState = ConnectionState.CONNECTING;
-                }
-            } else {
-                // Set different layout for each device
-                if (device.isMutingExpectedDevice()
-                        && !mController.isCurrentConnectedDeviceRemote()) {
-                    connectionState = ConnectionState.CONNECTED;
-                    restrictVolumeAdjustment = true;
-                    clickListener = v -> onItemClick(v, device);
-                } else if (currentlyConnected && isMutingExpectedDeviceExist
-                        && !mController.isCurrentConnectedDeviceRemote()) {
-                    // mark as disconnected and set special click listener
-                    clickListener = v -> cancelMuteAwaitConnection();
-                } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
-                    connectionState = ConnectionState.CONNECTING;
-                } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices()
-                        && isSelected) {
-                    if (mediaItem.isFirstDeviceInGroup()) {
-                        isDeviceGroup = true;
-                    } else {
-                        hideGroupItem = true;
-                    }
-                } else { // A connected or disconnected device.
-                    subtitle = device.hasSubtext() ? device.getSubtextString() : null;
-                    ongoingSessionStatus = getOngoingSessionStatus(device);
-                    groupStatus = getGroupStatus(isSelected, isSelectable, isDeselectable);
-
-                    if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
-                        deviceStatusIcon = mContext.getDrawable(
-                                R.drawable.media_output_status_failed);
-                        subtitle = mContext.getString(R.string.media_output_dialog_connect_failed);
-                        clickListener = v -> onItemClick(v, device);
-                    } else if (currentlyConnected || isSelected) {
-                        connectionState = ConnectionState.CONNECTED;
-                    } else { // disconnected
-                        if (isSelectable) { // groupable device
-                            if (!Flags.disableTransferWhenAppsDoNotSupport() || isTransferable
-                                    || hasRouteListingPreferenceItem) {
-                                clickListener = v -> onItemClick(v, device);
-                            }
-                        } else {
-                            deviceStatusIcon = getDeviceStatusIcon(device,
-                                    device.hasOngoingSession());
-                            clickListener = getClickListenerBasedOnSelectionBehavior(device);
-                        }
-                        deviceDisabled = clickListener == null;
-                    }
-                }
-            }
-
-            if (connectionState == ConnectionState.CONNECTED || isDeviceGroup) {
-                mCurrentActivePosition = position;
-            }
-
-            if (isDeviceGroup) {
-                renderDeviceGroupItem();
-            } else {
-                renderDeviceItem(hideGroupItem, device, connectionState, restrictVolumeAdjustment,
-                        groupStatus, ongoingSessionStatus, clickListener, deviceDisabled, subtitle,
-                        deviceStatusIcon);
-            }
-        }
-
-        private void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
-                ConnectionState connectionState, boolean restrictVolumeAdjustment,
-                GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
-                View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
-                Drawable deviceStatusIcon) {
-            if (hideGroupItem) {
-                mItemLayout.setVisibility(View.GONE);
-                return;
-            }
-            updateTitle(device.getName());
-            updateTitleIcon(device, connectionState, restrictVolumeAdjustment);
-            updateSeekBar(device, connectionState, restrictVolumeAdjustment,
-                    getDeviceItemContentDescription(device));
-            updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus);
-            updateLoadingIndicator(connectionState);
-            updateFullItemClickListener(clickListener);
-            updateContentAlpha(deviceDisabled);
-            updateSubtitle(subtitle);
-            updateDeviceStatusIcon(deviceStatusIcon);
-            updateItemBackground(connectionState);
-        }
-
-        private void renderDeviceGroupItem() {
-            String sessionName = mController.getSessionName() == null ? ""
-                    : mController.getSessionName().toString();
-            updateTitle(sessionName);
-            updateUnmutedVolumeIcon(null /* device */);
-            updateGroupSeekBar(getGroupItemContentDescription(sessionName));
-            updateEndAreaForDeviceGroup();
-            updateItemBackground(ConnectionState.CONNECTED);
-        }
-
-        private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) {
-            return device.hasOngoingSession() ? new OngoingSessionStatus(
-                    device.isHostForOngoingSession()) : null;
-        }
-
-        private GroupStatus getGroupStatus(boolean isSelected, boolean isSelectable,
-                boolean isDeselectable) {
-            // A device should either be selectable or, when the device selected, the list should
-            // have other selectable or selected devices.
-            boolean selectedWithOtherGroupDevices =
-                    isSelected && (hasMultipleSelectedDevices() || hasSelectableDevices());
-            if (isSelectable || selectedWithOtherGroupDevices) {
-                return new GroupStatus(isSelected, isDeselectable);
-            }
-            return null;
-        }
-
-        private boolean hasMultipleSelectedDevices() {
-            return mController.getSelectedMediaDevice().size() > 1;
-        }
-
-        private boolean hasSelectableDevices() {
-            return !mController.getSelectableMediaDevice().isEmpty();
-        }
-
-        /** Renders the right side round pill button / checkbox. */
-        private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState,
-                @Nullable GroupStatus groupStatus,
-                @Nullable OngoingSessionStatus ongoingSessionStatus) {
-            boolean showEndArea = false;
-            boolean isCheckbox = false;
-            // If both group status and the ongoing session status are present, only the ongoing
-            // session controls are displayed. The current layout design doesn't allow both group
-            // and ongoing session controls to be rendered simultaneously.
-            if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) {
-                showEndArea = true;
-                updateEndAreaForOngoingSession(device, ongoingSessionStatus.host());
-            } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
-                showEndArea = true;
-                isCheckbox = true;
-                updateEndAreaForGroupCheckBox(device, groupStatus);
-            }
-            updateEndAreaVisibility(showEndArea, isCheckbox);
-        }
-
-        private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
-            if (Flags.enableOutputSwitcherDeviceGrouping()) {
-                return isGroupCheckboxEnabled(groupStatus);
-            }
-            return true;
-        }
-
-        private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) {
-            boolean disabled = groupStatus.selected() && !groupStatus.deselectable();
-            return !disabled;
-        }
-
-        public void setCheckBoxColor(CheckBox checkBox, int color) {
-            int[][] states = {{android.R.attr.state_checked}, {}};
-            int[] colors = {color, color};
-            CompoundButtonCompat.setButtonTintList(checkBox, new
-                    ColorStateList(states, colors));
-        }
-
-        private void updateContentAlpha(boolean deviceDisabled) {
-            float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA;
-            mTitleIcon.setAlpha(alphaValue);
-            mTitleText.setAlpha(alphaValue);
-            mSubTitleText.setAlpha(alphaValue);
-            mStatusIcon.setAlpha(alphaValue);
-        }
-
-        private void updateEndAreaForDeviceGroup() {
-            updateEndAreaWithIcon(
-                    v -> {
-                        mShouldGroupSelectedMediaItems = false;
-                        notifyDataSetChanged();
-                    },
-                    R.drawable.media_output_item_expand_group,
-                    R.string.accessibility_expand_group);
-            updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */);
-        }
-
-        private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) {
-            updateEndAreaWithIcon(
-                    v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v),
-                    isHost ? R.drawable.media_output_status_edit_session
-                            : R.drawable.ic_sound_bars_anim,
-                    R.string.accessibility_open_application);
-        }
-
-        private void updateEndAreaWithIcon(View.OnClickListener clickListener,
-                @DrawableRes int iconDrawableId,
-                @StringRes int accessibilityStringId) {
-            updateEndAreaColor(mController.getColorSeekbarProgress());
-            mEndClickIcon.setImageTintList(
-                    ColorStateList.valueOf(mController.getColorItemContent()));
-            mEndClickIcon.setOnClickListener(clickListener);
-            mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick());
-            Drawable drawable = mContext.getDrawable(iconDrawableId);
-            mEndClickIcon.setImageDrawable(drawable);
-            if (drawable instanceof AnimatedVectorDrawable) {
-                ((AnimatedVectorDrawable) drawable).start();
-            }
-            if (Flags.enableOutputSwitcherDeviceGrouping()) {
-                mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
-            }
-        }
-
-        public void updateEndAreaColor(int color) {
-            mEndTouchArea.setBackgroundTintList(
-                    ColorStateList.valueOf(color));
-        }
-
-        @Nullable
-        private View.OnClickListener getClickListenerBasedOnSelectionBehavior(
-                @NonNull MediaDevice device) {
-            return Api34Impl.getClickListenerBasedOnSelectionBehavior(
-                    device, mController, v -> onItemClick(v, device));
-        }
-
-        @Nullable
-        private Drawable getDeviceStatusIcon(MediaDevice device, boolean hasOngoingSession) {
-            if (hasOngoingSession) {
-                return mContext.getDrawable(R.drawable.ic_sound_bars_anim);
-            } else {
-                return Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext);
-            }
-        }
-
-        void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) {
-            if (deviceStatusIcon == null) {
-                mStatusIcon.setVisibility(View.GONE);
-            } else {
-                mStatusIcon.setImageDrawable(deviceStatusIcon);
-                mStatusIcon.setImageTintList(
-                        ColorStateList.valueOf(mController.getColorItemContent()));
-                if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
-                    ((AnimatedVectorDrawable) deviceStatusIcon).start();
-                }
-                mStatusIcon.setVisibility(View.VISIBLE);
-            }
-        }
-
-        public void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
-                @NonNull GroupStatus groupStatus) {
-            boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
-            mEndTouchArea.setOnClickListener(
-                    isEnabled ? (v) -> mCheckBox.performClick() : null);
-            mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-            updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
-                    : mController.getColorItemBackground());
-            mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device));
-            mCheckBox.setOnCheckedChangeListener(null);
-            mCheckBox.setChecked(groupStatus.selected());
-            mCheckBox.setOnCheckedChangeListener(
-                    isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
-                            !groupStatus.selected(), device) : null);
-            mCheckBox.setEnabled(isEnabled);
-            setCheckBoxColor(mCheckBox, mController.getColorItemContent());
-        }
-
-        private void updateFullItemClickListener(@Nullable View.OnClickListener listener) {
-            mContainerLayout.setOnClickListener(listener);
-            updateIconAreaClickListener(listener);
-        }
-
-        /** Binds a ViewHolder for a "Connect a device" item. */
-        void onBindPairNewDevice() {
-            mTitleText.setTextColor(mController.getColorItemContent());
-            mCheckBox.setVisibility(View.GONE);
-            updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
-            updateItemBackground(ConnectionState.DISCONNECTED);
-            final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
-            mTitleIcon.setImageDrawable(addDrawable);
-            mTitleIcon.setImageTintList(
-                    ColorStateList.valueOf(mController.getColorItemContent()));
-            mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
-        }
-
-        private void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
-            disableSeekBar();
-            if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
-                mController.addDeviceToPlayMedia(device);
-            } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
-                    device)) {
-                mController.removeDeviceFromPlayMedia(device);
-            }
-        }
-
-        private void onItemClick(View view, MediaDevice device) {
-            if (mController.isCurrentOutputDeviceHasSessionOngoing()) {
-                showCustomEndSessionDialog(device);
-            } else {
-                transferOutput(device);
-            }
-        }
-
-        private void transferOutput(MediaDevice device) {
-            if (mController.isAnyDeviceTransferring()) {
-                return;
-            }
-            if (isCurrentlyConnected(device)) {
-                Log.d(TAG, "This device is already connected! : " + device.getName());
-                return;
-            }
-            mController.setTemporaryAllowListExceptionIfNeeded(device);
-            mCurrentActivePosition = -1;
-            mController.connectDevice(device);
-            device.setState(MediaDeviceState.STATE_CONNECTING);
-            notifyDataSetChanged();
-        }
-
-        @VisibleForTesting
-        void showCustomEndSessionDialog(MediaDevice device) {
-            MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
-                    mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
-                    mController.getColorItemContent());
-            mediaSessionReleaseDialog.show();
-        }
-
-        private void cancelMuteAwaitConnection() {
-            mController.cancelMuteAwaitConnection();
-            notifyDataSetChanged();
-        }
-
-        private String getDeviceItemContentDescription(@NonNull MediaDevice device) {
-            return mContext.getString(
-                    device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
-                            ? R.string.accessibility_bluetooth_name
-                            : R.string.accessibility_cast_name, device.getName());
-        }
-
-        private String getGroupItemContentDescription(String sessionName) {
-            return mContext.getString(R.string.accessibility_cast_name, sessionName);
-        }
-    }
-
-    class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder {
-        final TextView mTitleText;
-
-        MediaGroupDividerViewHolder(@NonNull View itemView) {
-            super(itemView);
-            mTitleText = itemView.requireViewById(R.id.title);
-        }
-
-        void onBind(String groupDividerTitle) {
-            mTitleText.setTextColor(mController.getColorItemContent());
-            mTitleText.setText(groupDividerTitle);
-        }
-    }
-
-    @RequiresApi(34)
-    private static class Api34Impl {
-        @DoNotInline
-        static View.OnClickListener getClickListenerBasedOnSelectionBehavior(
-                MediaDevice device,
-                MediaSwitchingController controller,
-                View.OnClickListener defaultTransferListener) {
-            switch (device.getSelectionBehavior()) {
-                case SELECTION_BEHAVIOR_NONE:
-                    return null;
-                case SELECTION_BEHAVIOR_TRANSFER:
-                    return defaultTransferListener;
-                case SELECTION_BEHAVIOR_GO_TO_APP:
-                    return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
-            }
-            return defaultTransferListener;
-        }
-
-        @DoNotInline
-        @Nullable
-        static Drawable getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device,
-                Context context) {
-            switch (device.getSelectionBehavior()) {
-                case SELECTION_BEHAVIOR_NONE:
-                    return context.getDrawable(R.drawable.media_output_status_failed);
-                case SELECTION_BEHAVIOR_TRANSFER:
-                    return null;
-                case SELECTION_BEHAVIOR_GO_TO_APP:
-                    return context.getDrawable(R.drawable.media_output_status_help);
-            }
-            return null;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
new file mode 100644
index 0000000..c58ba37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2020 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.media.dialog;
+
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.media.flags.Flags;
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.res.R;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * A parent RecyclerView adapter for the media output dialog device list. This class doesn't
+ * manipulate the layout directly.
+ */
+public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+    record OngoingSessionStatus(boolean host) {}
+
+    record GroupStatus(Boolean selected, Boolean deselectable) {}
+
+    enum ConnectionState {
+        CONNECTED,
+        CONNECTING,
+        DISCONNECTED,
+    }
+
+    protected final MediaSwitchingController mController;
+    private int mCurrentActivePosition;
+    private boolean mIsDragging;
+    private static final String TAG = "MediaOutputAdapterBase";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    protected final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
+    private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
+
+    public MediaOutputAdapterBase(MediaSwitchingController controller) {
+        mController = controller;
+        mCurrentActivePosition = -1;
+        mIsDragging = false;
+        setHasStableIds(true);
+    }
+
+    boolean isCurrentlyConnected(MediaDevice device) {
+        return TextUtils.equals(device.getId(),
+                mController.getCurrentConnectedMediaDevice().getId())
+                || (mController.getSelectedMediaDevice().size() == 1
+                && isDeviceIncluded(mController.getSelectedMediaDevice(), device));
+    }
+
+    boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
+        for (MediaDevice device : deviceList) {
+            if (TextUtils.equals(device.getId(), targetDevice.getId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    boolean isDragging() {
+        return mIsDragging;
+    }
+
+    void setIsDragging(boolean isDragging) {
+        mIsDragging = isDragging;
+    }
+
+    int getCurrentActivePosition() {
+        return mCurrentActivePosition;
+    }
+
+    /** Refreshes the RecyclerView dataset and forces re-render. */
+    public void updateItems() {
+        mMediaItemList.clear();
+        mMediaItemList.addAll(mController.getMediaItemList());
+        if (mShouldGroupSelectedMediaItems) {
+            if (mController.getSelectedMediaDevice().size() == 1) {
+                // Don't group devices if initially there isn't more than one selected.
+                mShouldGroupSelectedMediaItems = false;
+            }
+        }
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public long getItemId(int position) {
+        if (position >= mMediaItemList.size()) {
+            Log.d(TAG, "Incorrect position for item id: " + position);
+            return position;
+        }
+        MediaItem currentMediaItem = mMediaItemList.get(position);
+        return currentMediaItem.getMediaDevice().isPresent()
+                ? currentMediaItem.getMediaDevice().get().getId().hashCode()
+                : position;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (position >= mMediaItemList.size()) {
+            Log.d(TAG, "Incorrect position for item type: " + position);
+            return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER;
+        }
+        return mMediaItemList.get(position).getMediaItemType();
+    }
+
+    @Override
+    public int getItemCount() {
+        return mMediaItemList.size();
+    }
+
+    abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
+
+        Context mContext;
+
+        MediaDeviceViewHolderBase(View view, Context context) {
+            super(view);
+            mContext = context;
+        }
+
+        void renderItem(MediaItem mediaItem, int position) {
+            MediaDevice device = mediaItem.getMediaDevice().get();
+            boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
+            final boolean currentlyConnected = isCurrentlyConnected(device);
+            boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device);
+            boolean isDeselectable =
+                    isDeviceIncluded(mController.getDeselectableMediaDevice(), device);
+            boolean isSelectable = isDeviceIncluded(mController.getSelectableMediaDevice(), device);
+            boolean isTransferable =
+                    isDeviceIncluded(mController.getTransferableMediaDevices(), device);
+            boolean hasRouteListingPreferenceItem = device.hasRouteListingPreferenceItem();
+
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "["
+                                + position
+                                + "] "
+                                + device.getName()
+                                + " ["
+                                + (isDeselectable ? "deselectable" : "")
+                                + "] ["
+                                + (isSelected ? "selected" : "")
+                                + "] ["
+                                + (isSelectable ? "selectable" : "")
+                                + "] ["
+                                + (isTransferable ? "transferable" : "")
+                                + "] ["
+                                + (hasRouteListingPreferenceItem ? "hasListingPreference" : "")
+                                + "]");
+            }
+
+            boolean isDeviceGroup = false;
+            boolean hideGroupItem = false;
+            GroupStatus groupStatus = null;
+            OngoingSessionStatus ongoingSessionStatus = null;
+            ConnectionState connectionState = ConnectionState.DISCONNECTED;
+            boolean restrictVolumeAdjustment = mController.hasAdjustVolumeUserRestriction();
+            String subtitle = null;
+            Drawable deviceStatusIcon = null;
+            boolean deviceDisabled = false;
+            View.OnClickListener clickListener = null;
+
+            if (mCurrentActivePosition == position) {
+                mCurrentActivePosition = -1;
+            }
+
+            if (mController.isAnyDeviceTransferring()) {
+                if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
+                    connectionState = ConnectionState.CONNECTING;
+                }
+            } else {
+                // Set different layout for each device
+                if (device.isMutingExpectedDevice()
+                        && !mController.isCurrentConnectedDeviceRemote()) {
+                    connectionState = ConnectionState.CONNECTED;
+                    restrictVolumeAdjustment = true;
+                    clickListener = v -> onItemClick(v, device);
+                } else if (currentlyConnected && isMutingExpectedDeviceExist
+                        && !mController.isCurrentConnectedDeviceRemote()) {
+                    // mark as disconnected and set special click listener
+                    clickListener = v -> cancelMuteAwaitConnection();
+                } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
+                    connectionState = ConnectionState.CONNECTING;
+                } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices()
+                        && isSelected) {
+                    if (mediaItem.isFirstDeviceInGroup()) {
+                        isDeviceGroup = true;
+                    } else {
+                        hideGroupItem = true;
+                    }
+                } else { // A connected or disconnected device.
+                    subtitle = device.hasSubtext() ? device.getSubtextString() : null;
+                    ongoingSessionStatus = getOngoingSessionStatus(device);
+                    groupStatus = getGroupStatus(isSelected, isSelectable, isDeselectable);
+
+                    if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
+                        deviceStatusIcon = mContext.getDrawable(
+                                R.drawable.media_output_status_failed);
+                        subtitle = mContext.getString(R.string.media_output_dialog_connect_failed);
+                        clickListener = v -> onItemClick(v, device);
+                    } else if (currentlyConnected || isSelected) {
+                        connectionState = ConnectionState.CONNECTED;
+                    } else { // disconnected
+                        if (isSelectable) { // groupable device
+                            if (!Flags.disableTransferWhenAppsDoNotSupport() || isTransferable
+                                    || hasRouteListingPreferenceItem) {
+                                clickListener = v -> onItemClick(v, device);
+                            }
+                        } else {
+                            deviceStatusIcon = getDeviceStatusIcon(device,
+                                    device.hasOngoingSession());
+                            clickListener = getClickListenerBasedOnSelectionBehavior(device);
+                        }
+                        deviceDisabled = clickListener == null;
+                    }
+                }
+            }
+
+            if (connectionState == ConnectionState.CONNECTED || isDeviceGroup) {
+                mCurrentActivePosition = position;
+            }
+
+            if (isDeviceGroup) {
+                renderDeviceGroupItem();
+            } else {
+                renderDeviceItem(hideGroupItem, device, connectionState, restrictVolumeAdjustment,
+                        groupStatus, ongoingSessionStatus, clickListener, deviceDisabled, subtitle,
+                        deviceStatusIcon);
+            }
+        }
+
+        protected abstract void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
+                ConnectionState connectionState, boolean restrictVolumeAdjustment,
+                GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
+                View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
+                Drawable deviceStatusIcon);
+
+        protected abstract void renderDeviceGroupItem();
+
+        protected abstract void disableSeekBar();
+
+        private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) {
+            return device.hasOngoingSession() ? new OngoingSessionStatus(
+                    device.isHostForOngoingSession()) : null;
+        }
+
+        private GroupStatus getGroupStatus(boolean isSelected, boolean isSelectable,
+                boolean isDeselectable) {
+            // A device should either be selectable or, when the device selected, the list should
+            // have other selectable or selected devices.
+            boolean selectedWithOtherGroupDevices =
+                    isSelected && (hasMultipleSelectedDevices() || hasSelectableDevices());
+            if (isSelectable || selectedWithOtherGroupDevices) {
+                return new GroupStatus(isSelected, isDeselectable);
+            }
+            return null;
+        }
+
+        private boolean hasMultipleSelectedDevices() {
+            return mController.getSelectedMediaDevice().size() > 1;
+        }
+
+        private boolean hasSelectableDevices() {
+            return !mController.getSelectableMediaDevice().isEmpty();
+        }
+
+        @Nullable
+        private View.OnClickListener getClickListenerBasedOnSelectionBehavior(
+                @NonNull MediaDevice device) {
+            return Api34Impl.getClickListenerBasedOnSelectionBehavior(
+                    device, mController, v -> onItemClick(v, device));
+        }
+
+        @Nullable
+        private Drawable getDeviceStatusIcon(MediaDevice device, boolean hasOngoingSession) {
+            if (hasOngoingSession) {
+                return mContext.getDrawable(R.drawable.ic_sound_bars_anim);
+            } else {
+                return Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext);
+            }
+        }
+
+        protected void onExpandGroupButtonClicked() {
+            mShouldGroupSelectedMediaItems = false;
+            notifyDataSetChanged();
+        }
+
+        protected void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
+            disableSeekBar();
+            if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+                mController.addDeviceToPlayMedia(device);
+            } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
+                    device)) {
+                mController.removeDeviceFromPlayMedia(device);
+            }
+        }
+
+        private void onItemClick(View view, MediaDevice device) {
+            if (mController.isCurrentOutputDeviceHasSessionOngoing()) {
+                showCustomEndSessionDialog(device);
+            } else {
+                transferOutput(device);
+            }
+        }
+
+        private void transferOutput(MediaDevice device) {
+            if (mController.isAnyDeviceTransferring()) {
+                return;
+            }
+            if (isCurrentlyConnected(device)) {
+                Log.d(TAG, "This device is already connected! : " + device.getName());
+                return;
+            }
+            mController.setTemporaryAllowListExceptionIfNeeded(device);
+            mCurrentActivePosition = -1;
+            mController.connectDevice(device);
+            device.setState(MediaDeviceState.STATE_CONNECTING);
+            notifyDataSetChanged();
+        }
+
+        @VisibleForTesting
+        void showCustomEndSessionDialog(MediaDevice device) {
+            MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
+                    mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
+                    mController.getColorItemContent());
+            mediaSessionReleaseDialog.show();
+        }
+
+        private void cancelMuteAwaitConnection() {
+            mController.cancelMuteAwaitConnection();
+            notifyDataSetChanged();
+        }
+
+        protected String getDeviceItemContentDescription(@NonNull MediaDevice device) {
+            return mContext.getString(
+                    device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
+                            ? R.string.accessibility_bluetooth_name
+                            : R.string.accessibility_cast_name, device.getName());
+        }
+
+        protected String getGroupItemContentDescription(String sessionName) {
+            return mContext.getString(R.string.accessibility_cast_name, sessionName);
+        }
+    }
+
+    @RequiresApi(34)
+    private static class Api34Impl {
+        @DoNotInline
+        static View.OnClickListener getClickListenerBasedOnSelectionBehavior(
+                MediaDevice device,
+                MediaSwitchingController controller,
+                View.OnClickListener defaultTransferListener) {
+            switch (device.getSelectionBehavior()) {
+                case SELECTION_BEHAVIOR_NONE:
+                    return null;
+                case SELECTION_BEHAVIOR_TRANSFER:
+                    return defaultTransferListener;
+                case SELECTION_BEHAVIOR_GO_TO_APP:
+                    return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
+            }
+            return defaultTransferListener;
+        }
+
+        @DoNotInline
+        @Nullable
+        static Drawable getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device,
+                Context context) {
+            switch (device.getSelectionBehavior()) {
+                case SELECTION_BEHAVIOR_NONE:
+                    return context.getDrawable(R.drawable.media_output_status_failed);
+                case SELECTION_BEHAVIOR_TRANSFER:
+                    return null;
+                case SELECTION_BEHAVIOR_GO_TO_APP:
+                    return context.getDrawable(R.drawable.media_output_status_help);
+            }
+            return null;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
rename to packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index f97b3d3..565b2e4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -18,14 +18,18 @@
 
 import android.animation.Animator;
 import android.animation.ValueAnimator;
-import android.app.WallpaperColors;
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
 import android.graphics.drawable.LayerDrawable;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -40,6 +44,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.widget.CompoundButtonCompat;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.media.flags.Flags;
@@ -48,82 +53,67 @@
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.res.R;
 
-import java.util.List;
-
 /**
- * Base adapter for media output dialog.
+ * A RecyclerView adapter for the legacy UI media output dialog device list.
  */
-public abstract class MediaOutputBaseAdapter extends
-        RecyclerView.Adapter<RecyclerView.ViewHolder> {
-
-    record OngoingSessionStatus(boolean host) {}
-
-    record GroupStatus(Boolean selected, Boolean deselectable) {}
-
-    enum ConnectionState {
-        CONNECTED,
-        CONNECTING,
-        DISCONNECTED,
-    }
-
-    protected final MediaSwitchingController mController;
+public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
+    private static final String TAG = "MediaOutputAdapterL";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int UNMUTE_DEFAULT_VOLUME = 2;
-
-    Context mContext;
+    private static final float DEVICE_DISABLED_ALPHA = 0.5f;
+    private static final float DEVICE_ACTIVE_ALPHA = 1f;
     View mHolderView;
-    boolean mIsDragging;
-    int mCurrentActivePosition;
     private boolean mIsInitVolumeFirstTime;
 
-    public MediaOutputBaseAdapter(MediaSwitchingController controller) {
-        mController = controller;
-        mIsDragging = false;
-        mCurrentActivePosition = -1;
+    public MediaOutputAdapterLegacy(MediaSwitchingController controller) {
+        super(controller);
         mIsInitVolumeFirstTime = true;
     }
 
-    /**
-     * Refresh current dataset
-     */
-    public abstract void updateItems();
-
     @Override
     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
             int viewType) {
-        mContext = viewGroup.getContext();
-        mHolderView = LayoutInflater.from(mContext).inflate(MediaItem.getMediaLayoutId(viewType),
+
+        Context context = viewGroup.getContext();
+        mHolderView = LayoutInflater.from(viewGroup.getContext()).inflate(
+                MediaItem.getMediaLayoutId(viewType),
                 viewGroup, false);
 
-        return null;
-    }
-
-    void updateColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
-        mController.setCurrentColorScheme(wallpaperColors, isDarkTheme);
-    }
-
-    boolean isCurrentlyConnected(MediaDevice device) {
-        return TextUtils.equals(device.getId(),
-                mController.getCurrentConnectedMediaDevice().getId())
-                || (mController.getSelectedMediaDevice().size() == 1
-                && isDeviceIncluded(mController.getSelectedMediaDevice(), device));
-    }
-
-    boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
-        for (MediaDevice device : deviceList) {
-            if (TextUtils.equals(device.getId(), targetDevice.getId())) {
-                return true;
-            }
+        switch (viewType) {
+            case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
+                return new MediaGroupDividerViewHolderLegacy(mHolderView);
+            case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
+            case MediaItem.MediaItemType.TYPE_DEVICE:
+            default:
+                return new MediaDeviceViewHolderLegacy(mHolderView, context);
         }
-        return false;
     }
 
-    boolean isDragging() {
-        return mIsDragging;
-    }
-
-    int getCurrentActivePosition() {
-        return mCurrentActivePosition;
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
+        if (position >= getItemCount()) {
+            if (DEBUG) {
+                Log.d(TAG, "Incorrect position: " + position + " list size: "
+                        + getItemCount());
+            }
+            return;
+        }
+        MediaItem currentMediaItem = mMediaItemList.get(position);
+        switch (currentMediaItem.getMediaItemType()) {
+            case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
+                ((MediaGroupDividerViewHolderLegacy) viewHolder).onBind(
+                        currentMediaItem.getTitle());
+                break;
+            case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
+                ((MediaDeviceViewHolderLegacy) viewHolder).onBindPairNewDevice();
+                break;
+            case MediaItem.MediaItemType.TYPE_DEVICE:
+                ((MediaDeviceViewHolderLegacy) viewHolder).onBindDevice(currentMediaItem, position);
+                break;
+            default:
+                Log.d(TAG, "Incorrect position: " + position);
+        }
     }
 
     public MediaSwitchingController getController() {
@@ -133,7 +123,7 @@
     /**
      * ViewHolder for binding device view.
      */
-    abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+    class MediaDeviceViewHolderLegacy extends MediaDeviceViewHolderBase {
 
         private static final int ANIM_DURATION = 500;
 
@@ -158,8 +148,8 @@
         private ValueAnimator mVolumeAnimator;
         private int mLatestUpdateVolume = -1;
 
-        MediaDeviceBaseViewHolder(View view) {
-            super(view);
+        MediaDeviceViewHolderLegacy(View view, Context context) {
+            super(view, context);
             mContainerLayout = view.requireViewById(R.id.device_container);
             mItemLayout = view.requireViewById(R.id.item_layout);
             mTitleText = view.requireViewById(R.id.title);
@@ -180,8 +170,10 @@
             initAnimator();
         }
 
-        void onBind(MediaDevice device, int position) {
+        void onBindDevice(MediaItem mediaItem, int position) {
+            MediaDevice device = mediaItem.getMediaDevice().get();
             mDeviceId = device.getId();
+            mItemLayout.setVisibility(View.VISIBLE);
             mCheckBox.setVisibility(View.GONE);
             mStatusIcon.setVisibility(View.GONE);
             mEndTouchArea.setVisibility(View.GONE);
@@ -196,6 +188,54 @@
             mSeekBar.setProgressTintList(
                     ColorStateList.valueOf(mController.getColorSeekbarProgress()));
             enableFocusPropertyForView(mContainerLayout);
+            renderItem(mediaItem, position);
+        }
+
+        /** Binds a ViewHolder for a "Connect a device" item. */
+        void onBindPairNewDevice() {
+            mTitleText.setTextColor(mController.getColorItemContent());
+            mCheckBox.setVisibility(View.GONE);
+            updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
+            updateItemBackground(ConnectionState.DISCONNECTED);
+            final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
+            mTitleIcon.setImageDrawable(addDrawable);
+            mTitleIcon.setImageTintList(
+                    ColorStateList.valueOf(mController.getColorItemContent()));
+            mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
+        }
+
+        @Override
+        protected void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
+                ConnectionState connectionState, boolean restrictVolumeAdjustment,
+                GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
+                View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
+                Drawable deviceStatusIcon) {
+            if (hideGroupItem) {
+                mItemLayout.setVisibility(View.GONE);
+                return;
+            }
+            updateTitle(device.getName());
+            updateTitleIcon(device, connectionState, restrictVolumeAdjustment);
+            updateSeekBar(device, connectionState, restrictVolumeAdjustment,
+                    getDeviceItemContentDescription(device));
+            updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus);
+            updateLoadingIndicator(connectionState);
+            updateFullItemClickListener(clickListener);
+            updateContentAlpha(deviceDisabled);
+            updateSubtitle(subtitle);
+            updateDeviceStatusIcon(deviceStatusIcon);
+            updateItemBackground(connectionState);
+        }
+
+        @Override
+        protected void renderDeviceGroupItem() {
+            String sessionName = mController.getSessionName() == null ? ""
+                    : mController.getSessionName().toString();
+            updateTitle(sessionName);
+            updateUnmutedVolumeIcon(null /* device */);
+            updateGroupSeekBar(getGroupItemContentDescription(sessionName));
+            updateEndAreaForDeviceGroup();
+            updateItemBackground(ConnectionState.CONNECTED);
         }
 
         void updateTitle(CharSequence title) {
@@ -303,7 +343,7 @@
         private void initializeSeekbarVolume(
                 @Nullable MediaDevice device, int currentVolume,
                 boolean isCurrentSeekbarInvisible) {
-            if (!mIsDragging) {
+            if (!isDragging()) {
                 if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
                         || currentVolume == mLatestUpdateVolume)) {
                     // Update only if volume of device and value of volume bar doesn't match.
@@ -459,6 +499,132 @@
                     : R.drawable.media_output_icon_volume;
         }
 
+        private void updateContentAlpha(boolean deviceDisabled) {
+            float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA;
+            mTitleIcon.setAlpha(alphaValue);
+            mTitleText.setAlpha(alphaValue);
+            mSubTitleText.setAlpha(alphaValue);
+            mStatusIcon.setAlpha(alphaValue);
+        }
+
+        private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) {
+            if (deviceStatusIcon == null) {
+                mStatusIcon.setVisibility(View.GONE);
+            } else {
+                mStatusIcon.setImageDrawable(deviceStatusIcon);
+                mStatusIcon.setImageTintList(
+                        ColorStateList.valueOf(mController.getColorItemContent()));
+                if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
+                    ((AnimatedVectorDrawable) deviceStatusIcon).start();
+                }
+                mStatusIcon.setVisibility(View.VISIBLE);
+            }
+        }
+
+
+        /** Renders the right side round pill button / checkbox. */
+        private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState,
+                @Nullable GroupStatus groupStatus,
+                @Nullable OngoingSessionStatus ongoingSessionStatus) {
+            boolean showEndArea = false;
+            boolean isCheckbox = false;
+            // If both group status and the ongoing session status are present, only the ongoing
+            // session controls are displayed. The current layout design doesn't allow both group
+            // and ongoing session controls to be rendered simultaneously.
+            if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) {
+                showEndArea = true;
+                updateEndAreaForOngoingSession(device, ongoingSessionStatus.host());
+            } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
+                showEndArea = true;
+                isCheckbox = true;
+                updateEndAreaForGroupCheckBox(device, groupStatus);
+            }
+            updateEndAreaVisibility(showEndArea, isCheckbox);
+        }
+
+        private void updateEndAreaForDeviceGroup() {
+            updateEndAreaWithIcon(
+                    v -> {
+                        onExpandGroupButtonClicked();
+                    },
+                    R.drawable.media_output_item_expand_group,
+                    R.string.accessibility_expand_group);
+            updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */);
+        }
+
+        private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) {
+            updateEndAreaWithIcon(
+                    v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v),
+                    isHost ? R.drawable.media_output_status_edit_session
+                            : R.drawable.ic_sound_bars_anim,
+                    R.string.accessibility_open_application);
+        }
+
+        private void updateEndAreaWithIcon(View.OnClickListener clickListener,
+                @DrawableRes int iconDrawableId,
+                @StringRes int accessibilityStringId) {
+            updateEndAreaColor(mController.getColorSeekbarProgress());
+            mEndClickIcon.setImageTintList(
+                    ColorStateList.valueOf(mController.getColorItemContent()));
+            mEndClickIcon.setOnClickListener(clickListener);
+            mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick());
+            Drawable drawable = mContext.getDrawable(iconDrawableId);
+            mEndClickIcon.setImageDrawable(drawable);
+            if (drawable instanceof AnimatedVectorDrawable) {
+                ((AnimatedVectorDrawable) drawable).start();
+            }
+            if (Flags.enableOutputSwitcherDeviceGrouping()) {
+                mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
+            }
+        }
+
+        private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
+                @NonNull GroupStatus groupStatus) {
+            boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
+            mEndTouchArea.setOnClickListener(
+                    isEnabled ? (v) -> mCheckBox.performClick() : null);
+            mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
+                    : mController.getColorItemBackground());
+            mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device));
+            mCheckBox.setOnCheckedChangeListener(null);
+            mCheckBox.setChecked(groupStatus.selected());
+            mCheckBox.setOnCheckedChangeListener(
+                    isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
+                            !groupStatus.selected(), device) : null);
+            mCheckBox.setEnabled(isEnabled);
+            setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+        }
+
+        private void setCheckBoxColor(CheckBox checkBox, int color) {
+            int[][] states = {{android.R.attr.state_checked}, {}};
+            int[] colors = {color, color};
+            CompoundButtonCompat.setButtonTintList(checkBox, new
+                    ColorStateList(states, colors));
+        }
+
+        private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
+            if (Flags.enableOutputSwitcherDeviceGrouping()) {
+                return isGroupCheckboxEnabled(groupStatus);
+            }
+            return true;
+        }
+
+        private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) {
+            boolean disabled = groupStatus.selected() && !groupStatus.deselectable();
+            return !disabled;
+        }
+
+        private void updateEndAreaColor(int color) {
+            mEndTouchArea.setBackgroundTintList(
+                    ColorStateList.valueOf(color));
+        }
+
+        private void updateFullItemClickListener(@Nullable View.OnClickListener listener) {
+            mContainerLayout.setOnClickListener(listener);
+            updateIconAreaClickListener(listener);
+        }
+
         void updateIconAreaClickListener(@Nullable View.OnClickListener listener) {
             mIconAreaLayout.setOnClickListener(listener);
         }
@@ -498,6 +664,7 @@
             });
         }
 
+        @Override
         protected void disableSeekBar() {
             mSeekBar.setEnabled(false);
             mSeekBar.setOnTouchListener((v, event) -> true);
@@ -589,7 +756,7 @@
                 int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(
                         seekBar.getProgress());
                 mStartFromMute = (currentVolume == 0);
-                mIsDragging = true;
+                setIsDragging(true);
             }
 
             @Override
@@ -604,11 +771,25 @@
                 }
                 mTitleIcon.setVisibility(View.VISIBLE);
                 mVolumeValueText.setVisibility(View.GONE);
-                mIsDragging = false;
+                setIsDragging(false);
             }
             protected boolean shouldHandleProgressChanged() {
                 return mMediaDevice != null;
             }
         };
     }
+
+    class MediaGroupDividerViewHolderLegacy extends RecyclerView.ViewHolder {
+        final TextView mTitleText;
+
+        MediaGroupDividerViewHolderLegacy(@NonNull View itemView) {
+            super(itemView);
+            mTitleText = itemView.requireViewById(R.id.title);
+        }
+
+        void onBind(String groupDividerTitle) {
+            mTitleText.setTextColor(mController.getColorItemContent());
+            mTitleText.setText(groupDividerTitle);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 64256f9..d791361 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -105,7 +105,7 @@
     private boolean mIsLeBroadcastCallbackRegistered;
     private boolean mDismissing;
 
-    MediaOutputBaseAdapter mAdapter;
+    MediaOutputAdapterBase mAdapter;
 
     protected Executor mExecutor;
 
@@ -342,7 +342,7 @@
                 WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap());
                 colorSetUpdated = !wallpaperColors.equals(mWallpaperColors);
                 if (colorSetUpdated) {
-                    mAdapter.updateColorScheme(wallpaperColors, isDarkThemeOn);
+                    mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn);
                     updateButtonBackgroundColorFilter();
                     updateDialogBackgroundColor();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 9b5b872a..9ade9e2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -245,7 +245,7 @@
                 broadcastSender,
                 mediaSwitchingController, /* includePlaybackAndAppMetadata */
                 true);
-        mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
         //  that extends MediaOutputBaseDialog
         if (!aboveStatusbar) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index c9af7b3..2e602be 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -53,7 +53,7 @@
         super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
         mDialogTransitionAnimator = dialogTransitionAnimator;
         mUiEventLogger = uiEventLogger;
-        mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+        mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
         if (!aboveStatusbar) {
             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
index a0663d7..e293e20 100644
--- a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
+++ b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
@@ -25,7 +25,7 @@
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.modesApi() && Flags.modesUi()
+        get() = Flags.modesUi()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index c7b1654..c43c1a99 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -20,12 +20,15 @@
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.awaitCancellation
@@ -33,6 +36,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOf
 
 /**
  * Models UI state used to render the content of the notifications shade overlay.
@@ -47,6 +51,8 @@
     val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
     val sceneInteractor: SceneInteractor,
     private val shadeInteractor: ShadeInteractor,
+    disableFlagsInteractor: DisableFlagsInteractor,
+    mediaCarouselInteractor: MediaCarouselInteractor,
     activeNotificationsInteractor: ActiveNotificationsInteractor,
 ) : ExclusiveActivatable() {
 
@@ -69,6 +75,22 @@
                 ),
         )
 
+    val showMedia: Boolean by
+        hydrator.hydratedStateOf(
+            traceName = "showMedia",
+            initialValue =
+                disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled() &&
+                    mediaCarouselInteractor.hasActiveMediaOrRecommendation.value,
+            source =
+                disableFlagsInteractor.disableFlags.flatMapLatestConflated {
+                    if (it.isQuickSettingsEnabled()) {
+                        mediaCarouselInteractor.hasActiveMediaOrRecommendation
+                    } else {
+                        flowOf(false)
+                    }
+                },
+        )
+
     override suspend fun onActivated(): Nothing {
         coroutineScope {
             launch { hydrator.activate() }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
index 2082423..31323c7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -42,6 +42,8 @@
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -53,6 +55,7 @@
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class EditModeViewModel
 @Inject
@@ -73,11 +76,9 @@
     private val _isEditing = MutableStateFlow(false)
 
     /**
-     * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this
+     * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this.
      */
     val isEditing = _isEditing.asStateFlow()
-    private val minimumTiles: Int
-        get() = minTilesInteractor.minNumberOfTiles
 
     val gridLayout: StateFlow<GridLayout> =
         gridLayoutTypeInteractor.layout
@@ -99,7 +100,7 @@
      * * Tiles that are not available will be filtered out. None of them can be current (as they
      *   cannot be created), and they won't be able to be added.
      */
-    val tiles =
+    val tiles: Flow<List<EditTileViewModel>> =
         isEditing.flatMapLatest {
             if (it) {
                 val editTilesData = editTilesListInteractor.getTilesToEdit()
@@ -114,10 +115,10 @@
                 currentTilesInteractor.currentTiles
                     .map { tiles ->
                         val currentSpecs = tiles.map { it.spec }
-                        val canRemoveTiles = currentSpecs.size > minimumTiles
+                        val canRemoveTiles = currentSpecs.size > minTilesInteractor.minNumberOfTiles
                         val allTiles = editTilesData.stockTiles + editTilesData.customTiles
-                        val allTilesMap = allTiles.associate { it.tileSpec to it }
-                        val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
+                        val allTilesMap = allTiles.associateBy { it.tileSpec }
+                        val currentTiles = currentSpecs.mapNotNull { allTilesMap[it] }
                         val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
 
                         (currentTiles + nonCurrentTiles)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index c9a0635..61a8fa3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -55,6 +55,7 @@
         subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling
         subtitleIdsMap["hearing_devices"] = R.array.tile_states_hearing_devices
         subtitleIdsMap["notes"] = R.array.tile_states_notes
+        subtitleIdsMap["desktopeffects"] = R.array.tile_states_desktopeffects
     }
 
     /** Get the subtitle resource id of the given tile */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index c6bcab4..75cb8dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -589,8 +589,10 @@
                 }
                 mSecondaryMobileNetworkLayout = mDialogView.findViewById(
                         R.id.secondary_mobile_network_layout);
-                mSecondaryMobileNetworkLayout.setOnClickListener(
-                        this::onClickConnectedSecondarySub);
+                if (mCanConfigMobileData) {
+                    mSecondaryMobileNetworkLayout.setOnClickListener(
+                            this::onClickConnectedSecondarySub);
+                }
                 mSecondaryMobileNetworkLayout.setBackground(mBackgroundOn);
 
                 TextView mSecondaryMobileTitleText = mDialogView.requireViewById(
@@ -623,6 +625,8 @@
                         mDialogView.requireViewById(R.id.secondary_settings_icon);
                 mSecondaryMobileSettingsIcon.setColorFilter(
                         dialog.getContext().getColor(R.color.connected_network_primary_color));
+                mSecondaryMobileSettingsIcon.setVisibility(mCanConfigMobileData ?
+                        View.VISIBLE : View.INVISIBLE);
 
                 // set secondary visual for default data sub
                 mMobileNetworkLayout.setBackground(mBackgroundOff);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index caa7bba..e357f63 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -163,4 +163,12 @@
     fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
         _transitionState.value = transitionState
     }
+
+    /**
+     * If currently in a transition between contents, cancel that transition and go back to the
+     * pre-transition state.
+     */
+    fun freezeAndAnimateToCurrentState() {
+        dataSource.freezeAndAnimateToCurrentState()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index e9e7dec..0118085 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -234,6 +234,10 @@
      * The change is animated. Therefore, it will be some time before the UI will switch to the
      * desired scene. Once enough of the transition has occurred, the [currentScene] will become
      * [toScene] (unless the transition is canceled by user action or another call to this method).
+     *
+     * If [forceSettleToTargetScene] is `true` and the target scene is the same as the current
+     * scene, any current transition will be canceled and an animation to the target scene will be
+     * started.
      */
     @JvmOverloads
     fun changeScene(
@@ -241,9 +245,19 @@
         loggingReason: String,
         transitionKey: TransitionKey? = null,
         sceneState: Any? = null,
+        forceSettleToTargetScene: Boolean = false,
     ) {
         val currentSceneKey = currentScene.value
         val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
+
+        if (resolvedScene == currentSceneKey && forceSettleToTargetScene) {
+            logger.logSceneChangeCancellation(scene = resolvedScene, sceneState = sceneState)
+            onSceneAboutToChangeListener.forEach {
+                it.onSceneAboutToChange(resolvedScene, sceneState)
+            }
+            repository.freezeAndAnimateToCurrentState()
+        }
+
         if (
             !validateSceneChange(
                 from = currentSceneKey,
@@ -523,14 +537,32 @@
         }
 
         if (from == to) {
+            logger.logSceneChangeRejection(
+                from = from,
+                to = to,
+                originalChangeReason = loggingReason,
+                rejectionReason = "${from.debugName} is the same as ${to.debugName}",
+            )
             return false
         }
 
         if (to !in repository.allContentKeys) {
+            logger.logSceneChangeRejection(
+                from = from,
+                to = to,
+                originalChangeReason = loggingReason,
+                rejectionReason = "${to.debugName} isn't present in allContentKeys",
+            )
             return false
         }
 
         if (disabledContentInteractor.isDisabled(to)) {
+            logger.logSceneChangeRejection(
+                from = from,
+                to = to,
+                originalChangeReason = loggingReason,
+                rejectionReason = "${to.debugName} is currently disabled",
+            )
             return false
         }
 
@@ -580,14 +612,58 @@
         }
 
         if (to != null && disabledContentInteractor.isDisabled(to)) {
+            logger.logSceneChangeRejection(
+                from = from,
+                to = to,
+                originalChangeReason = loggingReason,
+                rejectionReason = "${to.debugName} is currently disabled",
+            )
             return false
         }
 
-        val isFromValid = (from == null) || (from in currentOverlays.value)
-        val isToValid =
-            (to == null) || (to !in currentOverlays.value && to in repository.allContentKeys)
+        return when {
+            to != null && from != null && to == from -> {
+                logger.logSceneChangeRejection(
+                    from = from,
+                    to = to,
+                    originalChangeReason = loggingReason,
+                    rejectionReason = "${from.debugName} is the same as ${to.debugName}",
+                )
+                false
+            }
 
-        return isFromValid && isToValid && from != to
+            to != null && to !in repository.allContentKeys -> {
+                logger.logSceneChangeRejection(
+                    from = from,
+                    to = to,
+                    originalChangeReason = loggingReason,
+                    rejectionReason = "${to.debugName} is not in allContentKeys",
+                )
+                false
+            }
+
+            from != null && from !in currentOverlays.value -> {
+                logger.logSceneChangeRejection(
+                    from = from,
+                    to = to,
+                    originalChangeReason = loggingReason,
+                    rejectionReason = "${from.debugName} is not a current overlay",
+                )
+                false
+            }
+
+            to != null && to in currentOverlays.value -> {
+                logger.logSceneChangeRejection(
+                    from = from,
+                    to = to,
+                    originalChangeReason = loggingReason,
+                    rejectionReason = "${to.debugName} is already a current overlay",
+                )
+                false
+            }
+
+            else -> true
+        }
     }
 
     /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
index 140b231..aab37d4 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.scene.domain.resolver
 
-import android.util.Log
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -84,7 +83,7 @@
         isDreamingWithOverlay: Boolean,
         isAbleToDream: Boolean,
     ): SceneKey {
-        val result = when {
+        return when {
             // Dream can run even if Keyguard is disabled, thus it has the highest priority here.
             isDreamingWithOverlay && isAbleToDream -> Scenes.Dream
             !isKeyguardEnabled -> Scenes.Gone
@@ -93,21 +92,9 @@
             !isUnlocked -> Scenes.Lockscreen
             else -> Scenes.Gone
         }
-        Log.d(TAG, "homeScene emitting $result, values:")
-        Log.d(TAG, "  isKeyguardEnabled=$isKeyguardEnabled")
-        Log.d(TAG, "  canSwipeToEnter=$canSwipeToEnter")
-        Log.d(TAG, "  isDeviceEntered=$isDeviceEntered" )
-        Log.d(TAG, "  isUnlocked=$isUnlocked")
-        Log.d(TAG, "  isDreamingWithOverlay=$isDreamingWithOverlay")
-        Log.d(TAG, "  isAbleToDream=$isAbleToDream")
-        Log.d(TAG, "")
-        return result
     }
 
     companion object {
-
-        private const val TAG = "HomeSceneFamilyResolver"
-
         val homeScenes =
             setOf(
                 Scenes.Gone,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 94e32fc..218ad47 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -90,6 +90,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
@@ -553,6 +554,7 @@
                         targetSceneKey = Scenes.Lockscreen,
                         loggingReason = "device is starting to sleep",
                         sceneState = keyguardInteractor.asleepKeyguardState.value,
+                        freezeAndAnimateToCurrentState = true,
                     )
                 } else {
                     val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
@@ -610,15 +612,24 @@
 
     private fun handleShadeTouchability() {
         applicationScope.launch {
-            shadeInteractor.isShadeTouchable
-                .distinctUntilChanged()
-                .filter { !it }
-                .collect {
-                    switchToScene(
-                        targetSceneKey = Scenes.Lockscreen,
-                        loggingReason = "device became non-interactive (SceneContainerStartable)",
-                    )
+            repeatWhen(deviceEntryInteractor.isDeviceEntered.map { !it }) {
+                // Run logic only when the device isn't entered.
+                repeatWhen(
+                    sceneInteractor.transitionState.map { !it.isTransitioning(to = Scenes.Gone) }
+                ) {
+                    // Run logic only when not transitioning to gone.
+                    shadeInteractor.isShadeTouchable
+                        .distinctUntilChanged()
+                        .filter { !it }
+                        .collect {
+                            switchToScene(
+                                targetSceneKey = Scenes.Lockscreen,
+                                loggingReason =
+                                    "device became non-interactive (SceneContainerStartable)",
+                            )
+                        }
                 }
+            }
         }
     }
 
@@ -923,11 +934,13 @@
         targetSceneKey: SceneKey,
         loggingReason: String,
         sceneState: Any? = null,
+        freezeAndAnimateToCurrentState: Boolean = false,
     ) {
         sceneInteractor.changeScene(
             toScene = targetSceneKey,
             loggingReason = loggingReason,
             sceneState = sceneState,
+            forceSettleToTargetScene = freezeAndAnimateToCurrentState,
         )
     }
 
@@ -1013,6 +1026,14 @@
         }
     }
 
+    private suspend fun repeatWhen(condition: Flow<Boolean>, block: suspend () -> Unit) {
+        condition.distinctUntilChanged().collectLatest { conditionMet ->
+            if (conditionMet) {
+                block()
+            }
+        }
+    }
+
     companion object {
         private const val TAG = "SceneContainerStartable"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index d005858..73c71f6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.scene.shared.logger
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
@@ -74,6 +75,50 @@
         )
     }
 
+    fun logSceneChangeCancellation(scene: SceneKey, sceneState: Any?) {
+        logBuffer.log(
+            tag = TAG,
+            level = LogLevel.INFO,
+            messageInitializer = {
+                str1 = scene.debugName
+                str2 = sceneState?.toString()
+            },
+            messagePrinter = { "CANCELED scene change. scene: $str1, sceneState: $str2" },
+        )
+    }
+
+    fun logSceneChangeRejection(
+        from: ContentKey?,
+        to: ContentKey?,
+        originalChangeReason: String,
+        rejectionReason: String,
+    ) {
+        logBuffer.log(
+            tag = TAG,
+            level = LogLevel.INFO,
+            messageInitializer = {
+                str1 = "${from?.debugName ?: "<none>"} → ${to?.debugName ?: "<none>"}"
+                str2 = rejectionReason
+                str3 = originalChangeReason
+                bool1 = to is OverlayKey
+            },
+            messagePrinter = {
+                buildString {
+                    append("REJECTED ")
+                    append(
+                        if (bool1) {
+                            "overlay "
+                        } else {
+                            "scene "
+                        }
+                    )
+                    append("change $str1 because \"$str2\" ")
+                    append("(original change reason: \"$str3\")")
+                }
+            },
+        )
+    }
+
     fun logSceneTransition(transitionState: ObservableTransitionState) {
         when (transitionState) {
             is ObservableTransitionState.Transition -> {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
index daf2d7f..42c4b24 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -83,4 +83,10 @@
 
     /** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */
     fun instantlyHideOverlay(overlay: OverlayKey)
+
+    /**
+     * If currently in a transition between contents, cancel that transition and go back to the
+     * pre-transition state.
+     */
+    fun freezeAndAnimateToCurrentState()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
index dcb6995..d6dce38 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -82,6 +82,10 @@
         delegateMutable.value.instantlyHideOverlay(overlay)
     }
 
+    override fun freezeAndAnimateToCurrentState() {
+        delegateMutable.value.freezeAndAnimateToCurrentState()
+    }
+
     /**
      * Binds the current, dependency injection provided [SceneDataSource] to the given object.
      *
@@ -120,5 +124,7 @@
         override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
 
         override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
+
+        override fun freezeAndAnimateToCurrentState() = Unit
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 305e71e..3be2f1b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -231,6 +231,9 @@
      */
     private var isDreaming = false
 
+    /** True if we should allow swiping open the glanceable hub. */
+    private var swipeToHubEnabled = false
+
     /** Observes and logs state when the lifecycle that controls the [touchMonitor] updates. */
     private val touchLifecycleLogger: LifecycleObserver = LifecycleEventObserver { _, event ->
         logger.d({
@@ -438,6 +441,7 @@
             },
         )
         collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
+        collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
 
         communalContainerWrapper = CommunalWrapper(containerView.context)
         communalContainerWrapper?.addView(communalContainerView)
@@ -520,10 +524,7 @@
         val glanceableHubV2 = communalSettingsInteractor.isV2FlagEnabled()
         if (
             !hubShowing &&
-                (touchOnNotifications ||
-                    touchOnUmo ||
-                    touchOnSmartspace ||
-                    !communalViewModel.swipeToHubEnabled())
+                (touchOnNotifications || touchOnUmo || touchOnSmartspace || !swipeToHubEnabled)
         ) {
             logger.d({
                 "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " +
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 5746cef..131efaa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2201,7 +2201,7 @@
     @Override
     @Deprecated
     public void onStatusBarLongPress(MotionEvent event) {
-        mShadeLog.d("Status Bar was long pressed.");
+        Log.i(TAG, "Status Bar was long pressed.");
         ShadeExpandsOnStatusBarLongPress.assertInNewMode();
         mStatusBarLongPressDowntime = event.getDownTime();
         if (isTracking()) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index f926d39..96b224f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -42,12 +42,12 @@
 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl
 import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor
-import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
+import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider
+import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
 import com.android.systemui.statusbar.policy.ConfigurationController
-import dagger.BindsOptionalOf
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ClassKey
@@ -67,7 +67,7 @@
  * By using this dedicated module, we ensure the notification shade window always utilizes the
  * correct display context and resources, regardless of the display it's on.
  */
-@Module(includes = [OptionalShadeDisplayAwareBindings::class, ShadeDisplayPolicyModule::class])
+@Module(includes = [ShadeDisplayPolicyModule::class])
 object ShadeDisplayAwareModule {
 
     /** Creates a new context for the shade window. */
@@ -242,17 +242,6 @@
         }
     }
 
-    @Provides
-    @IntoMap
-    @ClassKey(ShadeDisplaysInteractor::class)
-    fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable {
-        return if (ShadeWindowGoesAround.isEnabled) {
-            impl.get()
-        } else {
-            CoreStartable.NOP
-        }
-    }
-
     /**
      * Provided for making classes easier to test. In tests, a custom method to wait for the next
      * frame can be easily provided.
@@ -264,11 +253,25 @@
     fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true
 }
 
+/** Module that should be included only if the shade window [WindowRootView] is available. */
 @Module
-internal interface OptionalShadeDisplayAwareBindings {
-    @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView
+object ShadeDisplayAwareWithShadeWindowModule {
+    @Provides
+    @IntoMap
+    @ClassKey(ShadeDisplaysInteractor::class)
+    fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable {
+        return if (ShadeWindowGoesAround.isEnabled) {
+            impl.get()
+        } else {
+            CoreStartable.NOP
+        }
+    }
 
-    @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor
+    @Provides
+    @SysUISingleton
+    fun bindNotificationStackRebindingHider(
+        impl: NotificationStackRebindingHiderImpl
+    ): NotificationStackRebindingHider = impl
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
index 13b540a..5fda998 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
@@ -24,8 +24,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
-import com.android.systemui.util.kotlin.getOrNull
-import java.util.Optional
 import java.util.concurrent.CancellationException
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.seconds
@@ -51,22 +49,13 @@
 class ShadeDisplayChangeLatencyTracker
 @Inject
 constructor(
-    optionalShadeRootView: Optional<WindowRootView>,
+    private val shadeRootView: WindowRootView,
     @ShadeDisplayAware private val configurationRepository: ConfigurationRepository,
     private val latencyTracker: LatencyTracker,
     @Background private val bgScope: CoroutineScope,
     private val choreographerUtils: ChoreographerUtils,
 ) {
 
-    private val shadeRootView =
-        optionalShadeRootView.getOrNull()
-            ?: error(
-                """
-            ShadeRootView must be provided for ShadeDisplayChangeLatencyTracker to work.
-            If it is not, it means this is being instantiated in a SystemUI variant that shouldn't.
-            """
-                    .trimIndent()
-            )
     /**
      * We need to keep this always up to date eagerly to avoid delays receiving the new display ID.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 7d4b0ed..c44e066 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -54,7 +54,12 @@
 /** Module for classes related to the notification shade. */
 @Module(
     includes =
-        [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class]
+        [
+            StartShadeModule::class,
+            ShadeViewProviderModule::class,
+            WindowRootViewBlurModule::class,
+            ShadeDisplayAwareWithShadeWindowModule::class,
+        ]
 )
 abstract class ShadeModule {
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index e746274..9a5c968 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -39,9 +39,7 @@
 import com.android.systemui.statusbar.notification.row.NotificationRebindingTracker
 import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
-import com.android.systemui.util.kotlin.getOrNull
 import com.android.window.flags.Flags
-import java.util.Optional
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlin.time.Duration.Companion.seconds
@@ -63,17 +61,14 @@
     @Background private val bgScope: CoroutineScope,
     @Main private val mainThreadContext: CoroutineContext,
     private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker,
-    shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>,
+    private val shadeExpandedInteractor: ShadeExpandedStateInteractor,
     private val shadeExpansionIntent: ShadeExpansionIntent,
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val notificationRebindingTracker: NotificationRebindingTracker,
-    notificationStackRebindingHider: Optional<NotificationStackRebindingHider>,
+    private val notificationStackRebindingHider: NotificationStackRebindingHider,
     @ShadeDisplayAware private val configForwarder: ConfigurationForwarder,
 ) : CoreStartable {
 
-    private val shadeExpandedInteractor = requireOptional(shadeExpandedInteractor)
-    private val notificationStackRebindingHider = requireOptional(notificationStackRebindingHider)
-
     private val hasActiveNotifications: Boolean
         get() = activeNotificationsInteractor.areAnyNotificationsPresentValue
 
@@ -224,24 +219,5 @@
         const val TAG = "ShadeDisplaysInteractor"
         const val COLLAPSE_EXPAND_REASON = "Shade window move"
         val TIMEOUT = 1.seconds
-
-        /**
-         * [ShadeDisplaysInteractor] is bound in the SystemUI module for all variants, but needs
-         * some specific dependencies to be bound from each variant (e.g.
-         * [ShadeExpandedStateInteractor] or [NotificationStackRebindingHider]). When those are not
-         * bound, this class is not expected to be instantiated, and trying to instantiate it would
-         * crash.
-         */
-        inline fun <reified T> requireOptional(optional: Optional<T>): T {
-            return optional.getOrNull()
-                ?: error(
-                    """
-                ${T::class.java.simpleName} must be provided for ShadeDisplaysInteractor to work.
-                If it is not, it means this is being instantiated in a SystemUI variant that
-                shouldn't.
-                """
-                        .trimIndent()
-                )
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 9d81be2..e8b5d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade.domain.interactor
 
-import android.util.Log
 import com.android.app.tracing.coroutines.flow.flowName
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -39,7 +38,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
 /** The non-empty [ShadeInteractor] implementation. */
@@ -100,31 +98,17 @@
 
     override val isShadeTouchable: Flow<Boolean> =
         combine(
-            powerInteractor.isAsleep.onEach {
-                Log.d(TAG, "isShadeTouchable: upstream isAsleep=$it")
-            },
-            keyguardTransitionInteractor
-                .isInTransition(Edge.create(to = KeyguardState.AOD))
-                .onEach { Log.d(TAG, "isShadeTouchable: upstream isTransitioningToAod=$it") },
-            keyguardRepository.dozeTransitionModel
-                .map { it.to == DozeStateModel.DOZE_PULSING }
-                .onEach { Log.d(TAG, "isShadeTouchable: upstream isPulsing=$it") },
+            powerInteractor.isAsleep,
+            keyguardTransitionInteractor.isInTransition(Edge.create(to = KeyguardState.AOD)),
+            keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
         ) { isAsleep, isTransitioningToAod, isPulsing ->
-            val downstream =
-                when {
-                    // If the device is transitioning to AOD, only accept touches if
-                    // still animating.
-                    isTransitioningToAod -> dozeParams.shouldControlScreenOff()
-                    // If the device is asleep, only accept touches if there's a pulse
-                    isAsleep -> isPulsing
-                    else -> true
-                }
-            Log.d(TAG, "isShadeTouchable emitting $downstream, values:")
-            Log.d(TAG, "  isAsleep=$isAsleep")
-            Log.d(TAG, "  isTransitioningToAod=$isTransitioningToAod")
-            Log.d(TAG, "  isPulsing=$isPulsing")
-            Log.d(TAG, "")
-            downstream
+            when {
+                // If the device is transitioning to AOD, only accept touches if still animating.
+                isTransitioningToAod -> dozeParams.shouldControlScreenOff()
+                // If the device is asleep, only accept touches if there's a pulse
+                isAsleep -> isPulsing
+                else -> true
+            }
         }
 
     override val isExpandToQsEnabled: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index 8f4e870..1ab0b93 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.scene.domain.SceneFrameworkTableLog
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
@@ -32,6 +33,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -89,10 +91,14 @@
 ) : ShadeModeInteractor {
 
     private val isDualShadeEnabled: Flow<Boolean> =
-        secureSettingsRepository.boolSetting(
-            Settings.Secure.DUAL_SHADE,
-            defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
-        )
+        if (SceneContainerFlag.isEnabled) {
+            secureSettingsRepository.boolSetting(
+                Settings.Secure.DUAL_SHADE,
+                defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
+            )
+        } else {
+            flowOf(false)
+        }
 
     override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
index c23ff53..dc444ff 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade.shared.flag
 
+import android.window.DesktopExperienceFlags
 import com.android.systemui.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
@@ -30,10 +31,26 @@
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
+    /**
+     * This is defined as [DesktopExperienceFlags] to make it possible to enable it together with
+     * all the other desktop experience flags from the dev settings.
+     *
+     * Alternatively, using adb:
+     * ```bash
+     * adb shell aflags enable com.android.window.flags.show_desktop_experience_dev_option && \
+     *   adb shell setprop persist.wm.debug.desktop_experience_devopts 1
+     * ```
+     */
+    val FLAG =
+        DesktopExperienceFlags.DesktopExperienceFlag(
+            Flags::shadeWindowGoesAround,
+            /* shouldOverrideByDevOption= */ true,
+        )
+
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled: Boolean
-        get() = Flags.shadeWindowGoesAround()
+        get() = FLAG.isTrue
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
index b1af811..d8c3e25 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.notification.domain.interactor
 
+import com.android.systemui.activity.data.model.AppVisibilityModel
 import com.android.systemui.activity.data.repository.ActivityManagerRepository
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
@@ -30,8 +31,6 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
 
 /**
  * Interactor representing a single notification's status bar chip.
@@ -53,6 +52,7 @@
     @StatusBarChipsLog private val logBuffer: LogBuffer,
 ) {
     private val key = startingModel.key
+    private val uid = startingModel.uid
     private val logger = Logger(logBuffer, "Notif".pad())
     // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the
     // top-level tag. It should instead be provided as the first string in each log message.
@@ -88,28 +88,36 @@
             }
             return
         }
+
+        if (model.uid != uid) {
+            logger.e({
+                "$str1: received model with different uid, which shouldn't happen. " +
+                    "Original UID: $int1, New UID: $int2. " +
+                    "Proceeding as usual, but app visibility changes will be for *old* UID."
+            }) {
+                str1 = extraLogTag
+                int1 = uid
+                int2 = model.uid
+            }
+        }
         _notificationModel.value = model
     }
 
-    private val uid: Flow<Int> = _notificationModel.map { it.uid }
-
-    /** True if the application managing the notification is visible to the user. */
-    private val isAppVisible: Flow<Boolean> =
-        uid.flatMapLatest { currentUid ->
-            activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag)
-        }
+    /** Details about when the app managing the notification was & is visible to the user. */
+    private val appVisibility: Flow<AppVisibilityModel> =
+        activityManagerRepository.createAppVisibilityFlow(uid, logger, extraLogTag)
 
     /**
      * Emits this notification's status bar chip, or null if this notification shouldn't show a
      * status bar chip.
      */
     val notificationChip: Flow<NotificationChipModel?> =
-        combine(_notificationModel, isAppVisible) { notif, isAppVisible ->
-            notif.toNotificationChipModel(isAppVisible)
+        combine(_notificationModel, appVisibility) { notif, appVisibility ->
+            notif.toNotificationChipModel(appVisibility)
         }
 
     private fun ActiveNotificationModel.toNotificationChipModel(
-        isVisible: Boolean
+        appVisibility: AppVisibilityModel
     ): NotificationChipModel? {
         val promotedContent = this.promotedContent
         if (promotedContent == null) {
@@ -134,11 +142,13 @@
         }
 
         return NotificationChipModel(
-            key,
-            appName,
-            statusBarChipIconView,
-            promotedContent,
-            isVisible,
+            key = key,
+            appName = appName,
+            statusBarChipIconView = statusBarChipIconView,
+            promotedContent = promotedContent,
+            creationTime = creationTime,
+            isAppVisible = appVisibility.isAppCurrentlyVisible,
+            lastAppVisibleTime = appVisibility.lastAppVisibleTime,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
index c26d103..edb4418 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
+import kotlin.math.max
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -39,9 +40,11 @@
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 
 /** An interactor for the notification chips shown in the status bar. */
 @SysUISingleton
@@ -132,9 +135,6 @@
                         }
                     interactor.setNotification(notif)
                 }
-                logger.d({ "Interactors: $str1" }) {
-                    str1 = promotedNotificationInteractorMap.keys.joinToString(separator = " /// ")
-                }
                 promotedNotificationInteractors.value =
                     promotedNotificationInteractorMap.values.toList()
             }
@@ -145,26 +145,23 @@
      * Emits all notifications that are eligible to show as chips in the status bar. This is
      * different from which chips will *actually* show, see [shownNotificationChips] for that.
      */
-    private val allNotificationChips: Flow<List<NotificationChipModel>> =
+    val allNotificationChips: Flow<List<NotificationChipModel>> =
         if (StatusBarNotifChips.isEnabled) {
             // For all our current interactors...
-            promotedNotificationInteractors.flatMapLatest { intrs ->
-                // Stable-sort the promoted notifications by when they first appeared so that:
-                // 1) The chips don't switch places if the older chip gets a notification update.
-                // 2) The chips don't switch places when the second chip is tapped. (Whichever
-                // notification is showing heads-up is considered to be the top notification, which
-                // means tapping the second chip would move it to be the first chip if we didn't
-                // sort by appearance time here.)
-                // 3) Older chips get hidden if there's not enough room for all chips.
-                val interactors = intrs.sortedByDescending { it.creationTime }
+            // TODO(b/364653005): When a promoted notification is added or removed, each individual
+            // interactor's [notificationChip] flow becomes un-collected then re-collected, which
+            // can cause some flows to remove then add callbacks when they don't need to. Is there a
+            // better structure for this? Maybe Channels or a StateFlow with a short timeout?
+            promotedNotificationInteractors.flatMapLatest { interactors ->
                 if (interactors.isNotEmpty()) {
                     // Combine each interactor's [notificationChip] flow...
                     val allNotificationChips: List<Flow<NotificationChipModel?>> =
                         interactors.map { interactor -> interactor.notificationChip }
                     combine(allNotificationChips) {
-                        // ... and emit just the non-null chips
-                        it.filterNotNull()
-                    }
+                            // ... and emit just the non-null & sorted chips
+                            it.filterNotNull().sortedWith(chipComparator)
+                        }
+                        .logSort()
                 } else {
                     flowOf(emptyList())
                 }
@@ -181,4 +178,35 @@
             // out-of-sync (like a timer that's slightly off)
             chipsList.filter { !it.isAppVisible }
         }
+
+    /*
+    Stable sort the promoted notifications by two criteria:
+    Criteria #1: Whichever app was most recently visible has higher ranking.
+    - Reasoning: If a user opened the app to see additional information, that's
+    likely the most important ongoing notification.
+    Criteria #2: Whichever notification first appeared more recently has higher ranking.
+    - Reasoning: Older chips get hidden if there's not enough room for all chips.
+    This semi-stable ordering ensures:
+    1) The chips don't switch places if the older chip gets a notification update.
+    2) The chips don't switch places when the second chip is tapped. (Whichever
+    notification is showing heads-up is considered to be the top notification, which
+    means tapping the second chip would move it to be the first chip if we didn't
+    sort by appearance time here.)
+    */
+    private val chipComparator =
+        compareByDescending<NotificationChipModel> {
+            max(it.creationTime, it.lastAppVisibleTime ?: Long.MIN_VALUE)
+        }
+
+    private fun Flow<List<NotificationChipModel>>.logSort(): Flow<List<NotificationChipModel>> {
+        return this.distinctUntilChanged().onEach { chips ->
+            val logString =
+                chips.joinToString {
+                    "{key=${it.key}. " +
+                        "lastVisibleAppTime=${it.lastAppVisibleTime}. " +
+                        "creationTime=${it.creationTime}}"
+                }
+            logger.d({ "Sorted chips: $str1" }) { str1 = logString }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
index 97c3762..1f2079d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -26,6 +26,13 @@
     val appName: String,
     val statusBarChipIconView: StatusBarIconView?,
     val promotedContent: PromotedNotificationContentModel,
+    /** The time when the notification first appeared as promoted. */
+    val creationTime: Long,
     /** True if the app managing this notification is currently visible to the user. */
     val isAppVisible: Boolean,
+    /**
+     * The time when the app managing this notification last appeared as visible, or null if the app
+     * hasn't become visible since the notification became promoted.
+     */
+    val lastAppVisibleTime: Long?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 3ecbdf8..11e9fd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -51,6 +52,7 @@
     @Application private val applicationScope: CoroutineScope,
     private val notifChipsInteractor: StatusBarNotificationChipsInteractor,
     headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+    private val systemClock: SystemClock,
 ) {
     /**
      * A flow modeling the notification chips that should be shown. Emits an empty list if there are
@@ -158,16 +160,38 @@
                 clickBehavior,
             )
         }
+
         when (this.promotedContent.time.mode) {
             PromotedNotificationContentModel.When.Mode.BasicTime -> {
-                return OngoingActivityChipModel.Active.ShortTimeDelta(
-                    this.key,
-                    icon,
-                    colors,
-                    time = this.promotedContent.time.time,
-                    onClickListenerLegacy,
-                    clickBehavior,
-                )
+                return if (
+                    this.promotedContent.time.time >=
+                        systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
+                ) {
+                    OngoingActivityChipModel.Active.ShortTimeDelta(
+                        this.key,
+                        icon,
+                        colors,
+                        time = this.promotedContent.time.time,
+                        onClickListenerLegacy,
+                        clickBehavior,
+                    )
+                } else {
+                    // Don't show a `when` time that's close to now or in the past because it's
+                    // likely that the app didn't intentionally set the `when` time to be shown in
+                    // the status bar chip.
+                    // TODO(b/393369213): If a notification sets a `when` time in the future and
+                    // then that time comes and goes, the chip *will* start showing times in the
+                    // past. Not going to fix this right now because the Compose implementation
+                    // automatically handles this for us and we're hoping to launch the notification
+                    // chips at the same time as the Compose chips.
+                    return OngoingActivityChipModel.Active.IconOnly(
+                        this.key,
+                        icon,
+                        colors,
+                        onClickListenerLegacy,
+                        clickBehavior,
+                    )
+                }
             }
             PromotedNotificationContentModel.When.Mode.CountUp -> {
                 return OngoingActivityChipModel.Active.Timer(
@@ -204,4 +228,12 @@
             )
         )
     }
+
+    companion object {
+        /**
+         * Notifications must have a `when` time of at least 1 minute in the future in order for the
+         * status bar chip to show the time.
+         */
+        private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index d41353b..20dec11 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -408,11 +408,13 @@
     private fun View.setBackgroundPaddingForEmbeddedPaddingIcon() {
         val sidePadding =
             if (StatusBarNotifChips.isEnabled) {
-                0
-            } else {
                 context.resources.getDimensionPixelSize(
                     R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
                 )
+            } else {
+                context.resources.getDimensionPixelSize(
+                    R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon_legacy
+                )
             }
         setPaddingRelative(sidePadding, paddingTop, sidePadding, paddingBottom)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
index 4a999d5..efd402e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -41,7 +41,6 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import com.android.compose.animation.Expandable
 import com.android.compose.modifiers.thenIf
@@ -160,7 +159,10 @@
                     .padding(
                         horizontal =
                             if (hasEmbeddedIcon) {
-                                0.dp
+                                dimensionResource(
+                                    R.dimen
+                                        .ongoing_activity_chip_side_padding_for_embedded_padding_icon
+                                )
                             } else {
                                 dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
index 4017c43..3b8c0f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
@@ -24,7 +24,8 @@
 import androidx.compose.runtime.key
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.res.dimensionResource
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
 
@@ -35,10 +36,13 @@
     modifier: Modifier = Modifier,
 ) {
     Row(
-        // TODO(b/372657935): Remove magic numbers for padding and spacing.
-        modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp),
+        modifier =
+            modifier
+                .fillMaxHeight()
+                .padding(start = dimensionResource(R.dimen.ongoing_activity_chip_margin_start)),
         verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(8.dp),
+        horizontalArrangement =
+            Arrangement.spacedBy(dimensionResource(R.dimen.ongoing_activity_chip_margin_start)),
     ) {
         chips.active
             .filter { !it.isHidden }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt
new file mode 100644
index 0000000..e7bc052
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 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.featurepods.media
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * A [CoreStartable] that initializes and starts the media control chip functionality. The media
+ * chip is limited to large screen devices currently. Therefore, this [CoreStartable] should not be
+ * used for phones or smaller form factor devices.
+ */
+@SysUISingleton
+class MediaControlChipStartable
+@Inject
+constructor(
+    @Background val bgScope: CoroutineScope,
+    private val mediaControlChipInteractor: MediaControlChipInteractor,
+) : CoreStartable {
+
+    override fun start() {
+        bgScope.launch { mediaControlChipInteractor.initialize() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
index e3e77e1..f439bb2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
@@ -22,13 +22,15 @@
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -37,6 +39,8 @@
  * Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media
  * control chip. Emits a new [MediaControlChipModel] when there is an active media session and the
  * corresponding user preference is found, otherwise emits null.
+ *
+ * This functionality is only enabled on large screen devices.
  */
 @SysUISingleton
 class MediaControlChipInteractor
@@ -45,30 +49,57 @@
     @Background private val backgroundScope: CoroutineScope,
     mediaFilterRepository: MediaFilterRepository,
 ) {
-    private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> =
-        mediaFilterRepository.currentMedia
-            .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() }
-            .stateIn(
-                scope = backgroundScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = emptyList(),
-            )
+    private val isEnabled = MutableStateFlow(false)
+
+    private val mediaControlChipModelForScene: Flow<MediaControlChipModel?> =
+        combine(mediaFilterRepository.currentMedia, mediaFilterRepository.selectedUserEntries) {
+            mediaList,
+            userEntries ->
+            mediaList
+                .filterIsInstance<MediaCommonModel.MediaControl>()
+                .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] }
+                .firstOrNull { it.active }
+                ?.toMediaControlChipModel()
+        }
+
+    /**
+     * A flow of [MediaControlChipModel] representing the current state of the media controls chip.
+     * This flow emits null when no active media is playing or when playback information is
+     * unavailable. This flow is only active when [SceneContainerFlag] is disabled.
+     */
+    private val mediaControlChipModelLegacy = MutableStateFlow<MediaControlChipModel?>(null)
+
+    fun updateMediaControlChipModelLegacy(mediaData: MediaData?) {
+        if (!SceneContainerFlag.isEnabled) {
+            mediaControlChipModelLegacy.value = mediaData?.toMediaControlChipModel()
+        }
+    }
+
+    private val _mediaControlChipModel: Flow<MediaControlChipModel?> =
+        if (SceneContainerFlag.isEnabled) {
+            mediaControlChipModelForScene
+        } else {
+            mediaControlChipModelLegacy
+        }
 
     /** The currently active [MediaControlChipModel] */
-    val mediaControlModel: StateFlow<MediaControlChipModel?> =
-        combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) {
-                mediaControls,
-                userEntries ->
-                mediaControls
-                    .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] }
-                    .firstOrNull { it.active }
-                    ?.toMediaControlChipModel()
+    val mediaControlChipModel: StateFlow<MediaControlChipModel?> =
+        combine(_mediaControlChipModel, isEnabled) { mediaControlChipModel, isEnabled ->
+                if (isEnabled) {
+                    mediaControlChipModel
+                } else {
+                    null
+                }
             }
-            .stateIn(
-                scope = backgroundScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
+            .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null)
+
+    /**
+     * The media control chip may not be enabled on all form factors, so only the relevant form
+     * factors should initialize the interactor. This must be called from a CoreStartable.
+     */
+    fun initialize() {
+        isEnabled.value = true
+    }
 }
 
 private fun MediaData.toMediaControlChipModel(): MediaControlChipModel {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
index 19acb2e..90f97df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
@@ -55,8 +55,8 @@
      * whenever the underlying [MediaControlChipModel] changes.
      */
     override val chip: StateFlow<PopupChipModel> =
-        mediaControlChipInteractor.mediaControlModel
-            .map { mediaControlModel -> toPopupChipModel(mediaControlModel) }
+        mediaControlChipInteractor.mediaControlChipModel
+            .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) }
             .stateIn(
                 backgroundScope,
                 SharingStarted.WhileSubscribed(),
@@ -82,10 +82,6 @@
             chipId = PopupChipId.MediaControl,
             icon = defaultIcon,
             chipText = model.songName.toString(),
-            isToggled = false,
-            // TODO(b/385202114): Show a popup containing the media carousal when the chip is
-            // toggled.
-            onToggle = {},
             hoverBehavior = createHoverBehavior(model),
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
index 683b9716..6061553 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
@@ -53,10 +53,11 @@
         /** Default icon displayed on the chip */
         val icon: Icon,
         val chipText: String,
-        val isToggled: Boolean = false,
-        val onToggle: () -> Unit,
+        val isPopupShown: Boolean = false,
+        val showPopup: () -> Unit = {},
+        val hidePopup: () -> Unit = {},
         val hoverBehavior: HoverBehavior = HoverBehavior.None,
     ) : PopupChipModel() {
-        override val logName = "Shown(id=$chipId, toggled=$isToggled)"
+        override val logName = "Shown(id=$chipId, toggled=$isPopupShown)"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopup.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopup.kt
new file mode 100644
index 0000000..8a66904
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopup.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 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.featurepods.popups.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
+import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
+
+/**
+ * Displays a popup in the status bar area. The offset is calculated to draw the popup below the
+ * status bar.
+ */
+@Composable
+fun StatusBarPopup(viewModel: PopupChipModel.Shown) {
+    val density = Density(LocalContext.current)
+    Popup(
+        properties =
+            PopupProperties(
+                focusable = false,
+                dismissOnBackPress = true,
+                dismissOnClickOutside = true,
+            ),
+        offset =
+            IntOffset(
+                x = 0,
+                y = with(density) { dimensionResource(R.dimen.status_bar_height).roundToPx() },
+            ),
+        onDismissRequest = { viewModel.hidePopup() },
+    ) {
+        Box(modifier = Modifier.padding(8.dp).wrapContentSize()) {
+            when (viewModel.chipId) {
+                is PopupChipId.MediaControl -> {
+                    // TODO(b/385202114): Populate MediaControlPopup contents.
+                }
+            }
+            // Future popup types will be handled here.
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
index 34bef9d..eb85d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
@@ -52,14 +52,14 @@
  * the chip can show text containing contextual information.
  */
 @Composable
-fun StatusBarPopupChip(model: PopupChipModel.Shown, modifier: Modifier = Modifier) {
-    val hasHoverBehavior = model.hoverBehavior !is HoverBehavior.None
+fun StatusBarPopupChip(viewModel: PopupChipModel.Shown, modifier: Modifier = Modifier) {
+    val hasHoverBehavior = viewModel.hoverBehavior !is HoverBehavior.None
     val hoverInteractionSource = remember { MutableInteractionSource() }
     val isHovered by hoverInteractionSource.collectIsHoveredAsState()
-    val isToggled = model.isToggled
+    val isPopupShown = viewModel.isPopupShown
 
     val chipBackgroundColor =
-        if (isToggled) {
+        if (isPopupShown) {
             MaterialTheme.colorScheme.primaryContainer
         } else {
             MaterialTheme.colorScheme.surfaceContainerHighest
@@ -72,7 +72,7 @@
                 .padding(vertical = 4.dp)
                 .animateContentSize()
                 .thenIf(hasHoverBehavior) { Modifier.hoverable(hoverInteractionSource) }
-                .clickable { model.onToggle() },
+                .thenIf(!isPopupShown) { Modifier.clickable { viewModel.showPopup() } },
         color = chipBackgroundColor,
     ) {
         Row(
@@ -82,14 +82,14 @@
         ) {
             val iconColor =
                 if (isHovered) chipBackgroundColor else contentColorFor(chipBackgroundColor)
-            val hoverBehavior = model.hoverBehavior
+            val hoverBehavior = viewModel.hoverBehavior
             val iconBackgroundColor = contentColorFor(chipBackgroundColor)
             val iconInteractionSource = remember { MutableInteractionSource() }
             Icon(
                 icon =
                     when {
                         isHovered && hoverBehavior is HoverBehavior.Button -> hoverBehavior.icon
-                        else -> model.icon
+                        else -> viewModel.icon
                     },
                 modifier =
                     Modifier.thenIf(isHovered) {
@@ -109,7 +109,7 @@
             )
 
             Text(
-                text = model.chipText,
+                text = viewModel.chipText,
                 style = MaterialTheme.typography.labelLarge,
                 softWrap = false,
                 overflow = TextOverflow.Ellipsis,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt
index d35674d..16538c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt
@@ -31,10 +31,15 @@
     //    TODO(b/385353140): Add padding and spacing for this container according to UX specs.
     Box {
         Row(
-            modifier = Modifier.padding(horizontal = 8.dp),
+            modifier = modifier.padding(horizontal = 8.dp),
             verticalAlignment = Alignment.CenterVertically,
         ) {
-            chips.forEach { chip -> StatusBarPopupChip(chip) }
+            chips.forEach { chip ->
+                StatusBarPopupChip(chip)
+                if (chip.isPopupShown) {
+                    StatusBarPopup(chip)
+                }
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt
index caa8e6c..33bf90d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt
@@ -16,49 +16,65 @@
 
 package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel
 
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.MediaControlChipViewModel
 import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 
 /**
  * View model deciding which system process chips to show in the status bar. Emits a list of
  * PopupChipModels.
  */
-@SysUISingleton
 class StatusBarPopupChipsViewModel
-@Inject
-constructor(
-    @Background scope: CoroutineScope,
-    mediaControlChipViewModel: MediaControlChipViewModel,
-) {
+@AssistedInject
+constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable() {
+    private val hydrator: Hydrator = Hydrator("StatusBarPopupChipsViewModel.hydrator")
+
+    /** The ID of the current chip that is showing its popup, or `null` if no chip is shown. */
+    private var currentShownPopupChipId by mutableStateOf<PopupChipId?>(null)
+
+    private val incomingPopupChipBundle: PopupChipBundle by
+        hydrator.hydratedStateOf(
+            traceName = "incomingPopupChipBundle",
+            initialValue = PopupChipBundle(),
+            source = mediaControlChip.chip.map { chip -> PopupChipBundle(media = chip) },
+        )
+
+    val shownPopupChips: List<PopupChipModel.Shown> by derivedStateOf {
+        if (StatusBarPopupChips.isEnabled) {
+            val bundle = incomingPopupChipBundle
+
+            listOfNotNull(bundle.media).filterIsInstance<PopupChipModel.Shown>().map { chip ->
+                chip.copy(
+                    isPopupShown = chip.chipId == currentShownPopupChipId,
+                    showPopup = { currentShownPopupChipId = chip.chipId },
+                    hidePopup = { currentShownPopupChipId = null },
+                )
+            }
+        } else {
+            emptyList()
+        }
+    }
+
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
+
     private data class PopupChipBundle(
         val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControl)
     )
 
-    private val incomingPopupChipBundle: StateFlow<PopupChipBundle?> =
-        mediaControlChipViewModel.chip
-            .map { chip -> PopupChipBundle(media = chip) }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), PopupChipBundle())
-
-    val shownPopupChips: StateFlow<List<PopupChipModel.Shown>> =
-        if (StatusBarPopupChips.isEnabled) {
-            incomingPopupChipBundle
-                .map { bundle ->
-                    listOfNotNull(bundle?.media).filterIsInstance<PopupChipModel.Shown>()
-                }
-                .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
-        } else {
-            MutableStateFlow(emptyList<PopupChipModel.Shown>()).asStateFlow()
-        }
+    @AssistedFactory
+    interface Factory {
+        fun create(): StatusBarPopupChipsViewModel
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 37485fe..0e3f103 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -16,11 +16,74 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
+import static android.app.NotificationChannel.NEWS_ID;
+import static android.app.NotificationChannel.PROMOTIONS_ID;
+import static android.app.NotificationChannel.RECS_ID;
+import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+import java.util.List;
+
 /**
  * Abstract class to represent notification section bundled by AI.
  */
 public class BundleEntry extends PipelineEntry {
 
-    public class BundleEntryAdapter implements EntryAdapter {
+    private final String mKey;
+    private final BundleEntryAdapter mEntryAdapter;
+
+    // TODO (b/389839319): implement the row
+    private ExpandableNotificationRow mRow;
+
+    public BundleEntry(String key) {
+        mKey = key;
+        mEntryAdapter = new BundleEntryAdapter();
     }
+
+    @VisibleForTesting
+    public BundleEntryAdapter getEntryAdapter() {
+        return mEntryAdapter;
+    }
+
+    public class BundleEntryAdapter implements EntryAdapter {
+
+        /**
+         * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated?
+         */
+        @Override
+        public GroupEntry getParent() {
+            return GroupEntry.ROOT_ENTRY;
+        }
+
+        @Override
+        public boolean isTopLevelEntry() {
+            return true;
+        }
+
+        @Override
+        public String getKey() {
+            return mKey;
+        }
+
+        @Override
+        public ExpandableNotificationRow getRow() {
+            return mRow;
+        }
+
+        @Nullable
+        @Override
+        public EntryAdapter getGroupRoot() {
+            return this;
+        }
+    }
+
+    public static final List<BundleEntry> ROOT_BUNDLES = List.of(
+            new BundleEntry(PROMOTIONS_ID),
+            new BundleEntry(SOCIAL_MEDIA_ID),
+            new BundleEntry(NEWS_ID),
+            new BundleEntry(RECS_ID));
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index b12b1c5..4df81c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -16,8 +16,52 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
 /**
  * Adapter interface for UI to get relevant info.
  */
 public interface EntryAdapter {
+
+    /**
+     * Gets the parent of this entry, or null if the entry's view is not attached
+     */
+    @Nullable PipelineEntry getParent();
+
+    /**
+     * Returns whether the entry is attached and appears at the top level of the shade
+     */
+    boolean isTopLevelEntry();
+
+    /**
+     * @return the unique identifier for this entry
+     */
+    @NonNull String getKey();
+
+    /**
+     * Gets the view that this entry is backing.
+     */
+    @NonNull
+    ExpandableNotificationRow getRow();
+
+    /**
+     * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
+     * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
+     * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
+     * or a child of a group summary, the summary NotificationEntry will be returned, even if that
+     * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
+     * group or bundle grouping, null will be returned.
+     */
+    @Nullable
+    EntryAdapter getGroupRoot();
+
+    /**
+     * Returns whether the entry is attached to the current shade list
+     */
+    default boolean isAttached() {
+        return getParent() != null;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
index fc47dc1..8f3c357 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import javax.inject.Inject;
 
@@ -78,7 +79,7 @@
             requireBinder().inflateViews(
                     entry,
                     params,
-                    wrapInflationCallback(callback));
+                    wrapInflationCallback(entry, callback));
         } catch (InflationException e) {
             mLogger.logInflationException(entry, e);
             mNotifErrorManager.setInflationError(entry, e);
@@ -101,17 +102,26 @@
     }
 
     private NotificationRowContentBinder.InflationCallback wrapInflationCallback(
+            final NotificationEntry entry,
             InflationCallback callback) {
         return new NotificationRowContentBinder.InflationCallback() {
             @Override
             public void handleInflationException(
                     NotificationEntry entry,
                     Exception e) {
+                if (NotificationBundleUi.isEnabled()) {
+                    handleInflationException(e);
+                } else {
+                    mNotifErrorManager.setInflationError(entry, e);
+                }
+            }
+            @Override
+            public void handleInflationException(Exception e) {
                 mNotifErrorManager.setInflationError(entry, e);
             }
 
             @Override
-            public void onAsyncInflationFinished(NotificationEntry entry) {
+            public void onAsyncInflationFinished() {
                 mNotifErrorManager.clearInflationError(entry);
                 if (callback != null) {
                     callback.onInflationFinished(entry, entry.getRowController());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 7dd82a6..90f9525 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -29,6 +29,8 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
 
+import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES;
+import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED;
 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING;
 
@@ -107,6 +109,7 @@
     private final String mKey;
     private StatusBarNotification mSbn;
     private Ranking mRanking;
+    private final NotifEntryAdapter mEntryAdapter;
 
     /*
      * Bookkeeping members
@@ -268,9 +271,48 @@
         mKey = sbn.getKey();
         setSbn(sbn);
         setRanking(ranking);
+        mEntryAdapter = new NotifEntryAdapter();
     }
 
     public class NotifEntryAdapter implements EntryAdapter {
+        @Override
+        public PipelineEntry getParent() {
+            return NotificationEntry.this.getParent();
+        }
+
+        @Override
+        public boolean isTopLevelEntry() {
+            return getParent() != null
+                    && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent()));
+        }
+
+        @Override
+        public String getKey() {
+            return NotificationEntry.this.getKey();
+        }
+
+        @Override
+        public ExpandableNotificationRow getRow() {
+            return NotificationEntry.this.getRow();
+        }
+
+        @Nullable
+        @Override
+        public EntryAdapter getGroupRoot() {
+            // TODO (b/395857098): for backwards compatibility this will return null if called
+            // on a group summary that's not in a bundles, but it should return itself.
+            if (isTopLevelEntry() || getParent() == null) {
+                return null;
+            }
+            if (NotificationEntry.this.getParent().getSummary() != null) {
+                return NotificationEntry.this.getParent().getSummary().mEntryAdapter;
+            }
+            return null;
+        }
+    }
+
+    public EntryAdapter getEntryAdapter() {
+        return mEntryAdapter;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
index efedfef..c5a4791 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
@@ -19,5 +19,5 @@
 /**
  * Class to represent a notification, group, or bundle in the pipeline.
  */
-public class PipelineEntry {
+public abstract class PipelineEntry {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
index 733b986..9df4bf4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
@@ -23,6 +23,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -31,29 +32,50 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.PromotedNotificationsInteractor;
 import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
 
-import com.google.common.primitives.Booleans;
+import kotlinx.coroutines.CoroutineScope;
+
+import java.util.Collections;
+import java.util.List;
 
 import javax.inject.Inject;
 
 /**
  * Handles sectioning for foreground service notifications.
- *  Puts non-min colorized foreground service notifications into the FGS section. See
- *  {@link NotifCoordinators} for section ordering priority.
+ * Puts non-min colorized foreground service notifications into the FGS section. See
+ * {@link NotifCoordinators} for section ordering priority.
  */
 @CoordinatorScope
 public class ColorizedFgsCoordinator implements Coordinator {
     private static final String TAG = "ColorizedCoordinator";
+    private final PromotedNotificationsInteractor mPromotedNotificationsInteractor;
+    private final CoroutineScope mMainScope;
+
+    private List<String> mOrderedPromotedNotifKeys = Collections.emptyList();
 
     @Inject
-    public ColorizedFgsCoordinator() {
+    public ColorizedFgsCoordinator(
+            @Application CoroutineScope mainScope,
+            PromotedNotificationsInteractor promotedNotificationsInteractor
+    ) {
+        mPromotedNotificationsInteractor = promotedNotificationsInteractor;
+        mMainScope = mainScope;
     }
 
     @Override
-    public void attach(NotifPipeline pipeline) {
+    public void attach(@NonNull NotifPipeline pipeline) {
         if (PromotedNotificationUi.isEnabled()) {
             pipeline.addPromoter(mPromotedOngoingPromoter);
+
+            JavaAdapterKt.collectFlow(mMainScope,
+                    mPromotedNotificationsInteractor.getOrderedChipNotificationKeys(),
+                    (List<String> keys) -> {
+                        mOrderedPromotedNotifKeys = keys;
+                        mNotifSectioner.invalidateList("updated mOrderedPromotedNotifKeys");
+                    });
         }
     }
 
@@ -82,12 +104,24 @@
             return false;
         }
 
-        private NotifComparator mPreferPromoted = new NotifComparator("PreferPromoted") {
+        /** get the sort key for any entry in the ongoing section */
+        private int getSortKey(@Nullable NotificationEntry entry) {
+            if (entry == null) return Integer.MAX_VALUE;
+            // Order all promoted notif keys first, using their order in the list
+            final int index = mOrderedPromotedNotifKeys.indexOf(entry.getKey());
+            if (index >= 0) return index;
+            // Next, prioritize promoted ongoing over other notifications
+            return isPromotedOngoing(entry) ? Integer.MAX_VALUE - 1 : Integer.MAX_VALUE;
+        }
+
+        private final NotifComparator mOngoingComparator = new NotifComparator(
+                "OngoingComparator") {
             @Override
             public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) {
-                return -1 * Booleans.compare(
-                        isPromotedOngoing(o1.getRepresentativeEntry()),
-                        isPromotedOngoing(o2.getRepresentativeEntry()));
+                return Integer.compare(
+                        getSortKey(o1.getRepresentativeEntry()),
+                        getSortKey(o2.getRepresentativeEntry())
+                );
             }
         };
 
@@ -95,7 +129,7 @@
         @Override
         public NotifComparator getComparator() {
             if (PromotedNotificationUi.isEnabled()) {
-                return mPreferPromoted;
+                return mOngoingComparator;
             } else {
                 return null;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
index d47fe20..2e3ab92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import java.util.List;
 
@@ -129,21 +130,42 @@
                                 >= NotificationManager.IMPORTANCE_DEFAULT);
     }
 
+    /**
+     * Returns whether the given ListEntry has a high priority child or is in a group with a child
+     * that's high priority
+     */
     private boolean hasHighPriorityChild(ListEntry entry, boolean allowImplicit) {
-        if (entry instanceof NotificationEntry
-                && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) {
-            return false;
-        }
-
-        List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry);
-        if (children != null) {
-            for (NotificationEntry child : children) {
-                if (child != entry && isHighPriority(child, allowImplicit)) {
-                    return true;
+        if (NotificationBundleUi.isEnabled()) {
+            GroupEntry representativeGroupEntry = null;
+            if (entry instanceof GroupEntry) {
+                representativeGroupEntry = (GroupEntry) entry;
+            } else if (entry instanceof NotificationEntry){
+                final NotificationEntry notificationEntry = entry.getRepresentativeEntry();
+                if (notificationEntry.getParent() != null
+                        && notificationEntry.getParent().getSummary() != null
+                        && notificationEntry.getParent().getSummary() == notificationEntry) {
+                    representativeGroupEntry = notificationEntry.getParent();
                 }
             }
+            return representativeGroupEntry != null &&
+                    representativeGroupEntry.getChildren().stream().anyMatch(
+                            childEntry -> isHighPriority(childEntry, allowImplicit));
+
+        } else {
+            if (entry instanceof NotificationEntry
+                    && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) {
+                return false;
+            }
+            List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry);
+            if (children != null) {
+                for (NotificationEntry child : children) {
+                    if (child != entry && isHighPriority(child, allowImplicit)) {
+                        return true;
+                    }
+                }
+            }
+            return false;
         }
-        return false;
     }
 
     private boolean hasHighPriorityCharacteristics(NotificationEntry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
index 30386ab..ea36946 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.render;
 
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
@@ -38,6 +39,20 @@
     boolean isGroupExpanded(NotificationEntry entry);
 
     /**
+     * Whether the parent associated with this notification is expanded.
+     * If this notification is not part of a group or bundle, it will always return false.
+     */
+    boolean isGroupExpanded(EntryAdapter entry);
+
+    /**
+     * Set whether the group/bundle associated with this notification is expanded or not.
+     */
+    void setGroupExpanded(EntryAdapter entry, boolean expanded);
+
+    /** @return group/bundle expansion state after toggling. */
+    boolean toggleGroupExpansion(EntryAdapter entry);
+
+    /**
      * Set whether the group associated with this notification is expanded or not.
      */
     void setGroupExpanded(NotificationEntry entry, boolean expanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index d1aff80..16b98e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -23,11 +23,13 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -55,6 +57,8 @@
      */
     private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
 
+    private final Set<EntryAdapter> mExpandedCollections = new HashSet<>();
+
     @Inject
     public GroupExpansionManagerImpl(DumpManager dumpManager,
             GroupMembershipManager groupMembershipManager) {
@@ -63,11 +67,17 @@
     }
 
     /**
-     * Cleanup entries from mExpandedGroups that no longer exist in the pipeline.
+     * Cleanup entries from internal tracking that no longer exist in the pipeline.
      */
     private final OnBeforeRenderListListener mNotifTracker = (entries) -> {
-        if (mExpandedGroups.isEmpty()) {
-            return; // nothing to do
+        if (NotificationBundleUi.isEnabled())  {
+            if (mExpandedCollections.isEmpty()) {
+                return; // nothing to do
+            }
+        } else {
+            if (mExpandedGroups.isEmpty()) {
+                return; // nothing to do
+            }
         }
 
         final Set<NotificationEntry> renderingSummaries = new HashSet<>();
@@ -77,10 +87,25 @@
             }
         }
 
-        // If a group is in mExpandedGroups but not in the pipeline entries, collapse it.
-        final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries);
-        for (NotificationEntry entry : groupsToRemove) {
-            setGroupExpanded(entry, false);
+        if (NotificationBundleUi.isEnabled()) {
+            for (EntryAdapter entryAdapter : mExpandedCollections) {
+                boolean isInPipeline = false;
+                for (NotificationEntry entry : renderingSummaries) {
+                    if (entry.getKey().equals(entryAdapter.getKey())) {
+                        isInPipeline = true;
+                        break;
+                    }
+                }
+                if (!isInPipeline) {
+                    setGroupExpanded(entryAdapter, false);
+                }
+            }
+        } else {
+            // If a group is in mExpandedGroups but not in the pipeline entries, collapse it.
+            final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries);
+            for (NotificationEntry entry : groupsToRemove) {
+                setGroupExpanded(entry, false);
+            }
         }
     };
 
@@ -96,11 +121,13 @@
 
     @Override
     public boolean isGroupExpanded(NotificationEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
         return mExpandedGroups.contains(mGroupMembershipManager.getGroupSummary(entry));
     }
 
     @Override
     public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
+        NotificationBundleUi.assertInLegacyMode();
         NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
         if (entry.getParent() == null) {
             if (expanded) {
@@ -127,14 +154,61 @@
 
     @Override
     public boolean toggleGroupExpansion(NotificationEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
+        setGroupExpanded(entry, !isGroupExpanded(entry));
+        return isGroupExpanded(entry);
+    }
+
+    @Override
+    public boolean isGroupExpanded(EntryAdapter entry) {
+        NotificationBundleUi.assertInNewMode();
+        return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry));
+    }
+
+    @Override
+    public void setGroupExpanded(EntryAdapter entry, boolean expanded) {
+        NotificationBundleUi.assertInNewMode();
+        EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry);
+        if (!entry.isAttached()) {
+            if (expanded) {
+                Log.wtf(TAG, "Cannot expand group that is not attached");
+            } else {
+                // The entry is no longer attached, but we still want to make sure we don't have
+                // a stale expansion state.
+                groupParent = entry;
+            }
+        }
+
+        boolean changed;
+        if (expanded) {
+            changed = mExpandedCollections.add(groupParent);
+        } else {
+            changed = mExpandedCollections.remove(groupParent);
+        }
+
+        // Only notify listeners if something changed.
+        if (changed) {
+            sendOnGroupExpandedChange(entry, expanded);
+        }
+    }
+
+    @Override
+    public boolean toggleGroupExpansion(EntryAdapter entry) {
+        NotificationBundleUi.assertInNewMode();
         setGroupExpanded(entry, !isGroupExpanded(entry));
         return isGroupExpanded(entry);
     }
 
     @Override
     public void collapseGroups() {
-        for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) {
-            setGroupExpanded(entry, false);
+        if (NotificationBundleUi.isEnabled()) {
+            for (EntryAdapter entry : new ArrayList<>(mExpandedCollections)) {
+                setGroupExpanded(entry, false);
+            }
+        } else {
+            for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) {
+                setGroupExpanded(entry, false);
+            }
         }
     }
 
@@ -145,9 +219,21 @@
         for (NotificationEntry entry : mExpandedGroups) {
             pw.println("  * " + entry.getKey());
         }
+        pw.println("  mExpandedCollection: " + mExpandedCollections.size());
+        for (EntryAdapter entry : mExpandedCollections) {
+            pw.println("  * " + entry.getKey());
+        }
     }
 
     private void sendOnGroupExpandedChange(NotificationEntry entry, boolean expanded) {
+        NotificationBundleUi.assertInLegacyMode();
+        for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) {
+            listener.onGroupExpansionChange(entry.getRow(), expanded);
+        }
+    }
+
+    private void sendOnGroupExpandedChange(EntryAdapter entry, boolean expanded) {
+        NotificationBundleUi.assertInNewMode();
         for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) {
             listener.onGroupExpansionChange(entry.getRow(), expanded);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
index 3158782..69267e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
@@ -29,6 +30,13 @@
  * generally assumes that the notification is attached (aka its parent is not null).
  */
 public interface GroupMembershipManager {
+
+    /**
+     * @return whether a given entry is the root (GroupEntry or BundleEntry) in a collection which
+     * has children
+     */
+    boolean isGroupRoot(@NonNull EntryAdapter entry);
+
     /**
      * @return whether a given notification is the summary in a group which has children
      */
@@ -42,16 +50,15 @@
     NotificationEntry getGroupSummary(@NonNull NotificationEntry entry);
 
     /**
-     * Similar to {@link #getGroupSummary(NotificationEntry)} but doesn't get the visual summary
-     * but the logical summary, i.e when a child is isolated, it still returns the summary as if
-     * it wasn't isolated.
-     * TODO: remove this when migrating to the new pipeline, this is taken care of in the
-     * dismissal logic built into NotifCollection
+     * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
+     * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
+     * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
+     * or a child of a group summary, the summary NotificationEntry will be returned, even if that
+     * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
+     * group or bundle grouping, null will be returned.
      */
     @Nullable
-    default NotificationEntry getLogicalGroupSummary(@NonNull NotificationEntry entry) {
-        return getGroupSummary(entry);
-    }
+    EntryAdapter getGroupRoot(@NonNull EntryAdapter entry);
 
     /**
      * @return whether a given notification is a child in a group
@@ -59,9 +66,10 @@
     boolean isChildInGroup(@NonNull NotificationEntry entry);
 
     /**
-     * Whether this is the only child in a group
+     * @return whether a given notification is a child in a group. The group may be a notification
+     * group or a bundle.
      */
-    boolean isOnlyChildInGroup(@NonNull NotificationEntry entry);
+    boolean isChildInGroup(@NonNull EntryAdapter entry);
 
     /**
      * Get the children that are in the summary's group, not including those isolated.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index da12479..80a9f8ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -22,9 +22,11 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import java.util.List;
 
@@ -41,6 +43,7 @@
 
     @Override
     public boolean isGroupSummary(@NonNull NotificationEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
         if (entry.getParent() == null) {
             // The entry is not attached, so it doesn't count.
             return false;
@@ -49,33 +52,47 @@
         return entry.getParent().getSummary() == entry;
     }
 
+    @Override
+    public boolean isGroupRoot(@NonNull EntryAdapter entry) {
+        NotificationBundleUi.assertInNewMode();
+        return entry == entry.getGroupRoot();
+    }
+
     @Nullable
     @Override
     public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
         if (isTopLevelEntry(entry) || entry.getParent() == null) {
             return null;
         }
         return entry.getParent().getSummary();
     }
 
+    @Nullable
+    @Override
+    public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) {
+        NotificationBundleUi.assertInNewMode();
+        return entry.getGroupRoot();
+    }
+
     @Override
     public boolean isChildInGroup(@NonNull NotificationEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
         // An entry is a child if it's not a summary or top level entry, but it is attached.
         return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null;
     }
 
     @Override
-    public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) {
-        if (entry.getParent() == null) {
-            return false; // The entry is not attached.
-        }
-
-        return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1;
+    public boolean isChildInGroup(@NonNull EntryAdapter entry) {
+        NotificationBundleUi.assertInNewMode();
+        // An entry is a child if it's not a group root or top level entry, but it is attached.
+        return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry();
     }
 
     @Nullable
     @Override
     public List<NotificationEntry> getChildren(@NonNull ListEntry entry) {
+        NotificationBundleUi.assertInLegacyMode();
         if (entry instanceof GroupEntry) {
             return ((GroupEntry) entry).getChildren();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
deleted file mode 100644
index bcaf187..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2025 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.notification.dagger
-
-import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider
-import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl
-import dagger.Binds
-import dagger.BindsOptionalOf
-import dagger.Module
-
-/**
- * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController].
- */
-@Module
-interface NotificationStackModule {
-    @Binds
-    fun bindNotificationStackRebindingHider(
-        impl: NotificationStackRebindingHiderImpl
-    ): NotificationStackRebindingHider
-}
-
-/** This is meant to be used by all SystemUI variants, also those without NSSL. */
-@Module
-interface NotificationStackOptionalModule {
-    @BindsOptionalOf
-    fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 34f4969..53d5dbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -121,7 +121,6 @@
         NotificationMemoryModule.class,
         NotificationStatsLoggerModule.class,
         NotificationsLogModule.class,
-        NotificationStackOptionalModule.class,
 })
 public interface NotificationsModule {
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index be61dc9..7d74a49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -46,6 +46,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -55,6 +56,7 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
 import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -118,7 +120,8 @@
     @VisibleForTesting
     final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>();
     private final HeadsUpManagerLogger mLogger;
-    private final int mMinimumDisplayTime;
+    private final int mMinimumDisplayTimeDefault;
+    private final int mMinimumDisplayTimeForUserInitiated;
     private final int mStickyForSomeTimeAutoDismissTime;
     private final int mAutoDismissTime;
     private final DelayableExecutor mExecutor;
@@ -215,9 +218,11 @@
         mGroupMembershipManager = groupMembershipManager;
         mVisualStabilityProvider = visualStabilityProvider;
         Resources resources = context.getResources();
-        mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
+        mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled()
                 ? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling)
                 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
+        mMinimumDisplayTimeForUserInitiated = resources.getInteger(
+                R.integer.heads_up_notification_minimum_time_for_user_initiated);
         mStickyForSomeTimeAutoDismissTime = resources.getInteger(
                 R.integer.sticky_heads_up_notification_time);
         mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay);
@@ -871,14 +876,24 @@
         if (!hasPinnedHeadsUp() || topEntry == null) {
             return null;
         } else {
+            ExpandableNotificationRow topRow = topEntry.getRow();
             if (topEntry.rowIsChildInGroup()) {
-                final NotificationEntry groupSummary =
-                        mGroupMembershipManager.getGroupSummary(topEntry);
-                if (groupSummary != null) {
-                    topEntry = groupSummary;
+                if (NotificationBundleUi.isEnabled()) {
+                    final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot(
+                            topRow.getEntryAdapter());
+                    if (adapter != null) {
+                        topRow = adapter.getRow();
+                    }
+                } else {
+                    final NotificationEntry groupSummary =
+                            mGroupMembershipManager.getGroupSummary(topEntry);
+                    if (groupSummary != null) {
+                        topEntry = groupSummary;
+                        topRow = topEntry.getRow();
+                    }
                 }
             }
-            ExpandableNotificationRow topRow = topEntry.getRow();
+
             int[] tmpArray = new int[2];
             topRow.getLocationOnScreen(tmpArray);
             int minX = tmpArray[0];
@@ -1358,7 +1373,12 @@
 
                 final long now = mSystemClock.elapsedRealtime();
                 if (updateEarliestRemovalTime) {
-                    mEarliestRemovalTime = now + mMinimumDisplayTime;
+                    if (StatusBarNotifChips.isEnabled()
+                            && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) {
+                        mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated;
+                    } else {
+                        mEarliestRemovalTime = now + mMinimumDisplayTimeDefault;
+                    }
                 }
 
                 if (updatePostTime) {
@@ -1377,7 +1397,7 @@
                 final long now = mSystemClock.elapsedRealtime();
                 return NotificationThrottleHun.isEnabled()
                         ? Math.max(finishTime, mEarliestRemovalTime) - now
-                        : Math.max(finishTime - now, mMinimumDisplayTime);
+                        : Math.max(finishTime - now, mMinimumDisplayTimeDefault);
             };
             scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
index 3b6b9ed..47e725c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.headsup;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.os.RemoteException;
 import android.view.MotionEvent;
@@ -210,10 +211,12 @@
                         if (mHeadsUpManager.shouldSwallowClick(
                                 mPickedChild.getEntry().getSbn().getKey())) {
                             endMotion();
+                            setTrackingHeadsUp(false);
                             return true;
                         }
                     }
                     endMotion();
+                    setTrackingHeadsUp(false);
                     return false;
             }
             return false;
@@ -258,7 +261,7 @@
         void setHeadsUpDraggingStartingHeight(int startHeight);
 
         /** Sets notification that is being expanded. */
-        void setTrackedHeadsUp(ExpandableNotificationRow expandableNotificationRow);
+        void setTrackedHeadsUp(@Nullable ExpandableNotificationRow expandableNotificationRow);
 
         /** Called when a MotionEvent is about to trigger expansion. */
         void startExpand(float newX, float newY, boolean startTracking, float expandedHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
index 691f1f4..f755dbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import javax.inject.Inject
 import kotlin.math.max
 
@@ -112,14 +113,26 @@
             if (personExtractor.isPersonNotification(sbn)) TYPE_PERSON else TYPE_NON_PERSON
 
     private fun getPeopleTypeOfSummary(entry: NotificationEntry): Int {
-        if (!groupManager.isGroupSummary(entry)) {
-            return TYPE_NON_PERSON
-        }
+        if (NotificationBundleUi.isEnabled) {
+            if (!entry.sbn.notification.isGroupSummary) {
+                return TYPE_NON_PERSON;
+            }
 
-        val childTypes = groupManager.getChildren(entry)
-                ?.asSequence()
-                ?.map { getPeopleNotificationType(it) }
-                ?: return TYPE_NON_PERSON
+            return getPeopleTypeForChildList(entry.parent?.children)
+        } else {
+            if (!groupManager.isGroupSummary(entry)) {
+                return TYPE_NON_PERSON
+            }
+
+            return getPeopleTypeForChildList(groupManager.getChildren(entry))
+        }
+    }
+
+    private fun getPeopleTypeForChildList(children: List<NotificationEntry>?): Int {
+        val childTypes = children
+            ?.asSequence()
+            ?.map { getPeopleNotificationType(it) }
+            ?: return TYPE_NON_PERSON
 
         var groupType = TYPE_NON_PERSON
         for (childType in childTypes) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
index e5d2361..893570b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
@@ -426,6 +426,8 @@
 
         chronometer = chronometerStub?.inflate() as Chronometer
         chronometerStub = null
+
+        chronometer?.appendFontFeatureSetting("tnum")
     }
 
     private fun inflateOldProgressBar() {
@@ -501,6 +503,10 @@
     }
 }
 
+private fun TextView.appendFontFeatureSetting(newSetting: String) {
+    fontFeatureSettings = (fontFeatureSettings?.let { "$it," } ?: "") + newSetting
+}
+
 private enum class AodPromotedNotificationColor(val colorInt: Int) {
     Background(android.graphics.Color.BLACK),
     PrimaryText(android.graphics.Color.WHITE),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
index 393f95d..4bc6854 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
 import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -30,13 +29,12 @@
 class AODPromotedNotificationInteractor
 @Inject
 constructor(
-    activeNotificationsInteractor: ActiveNotificationsInteractor,
+    promotedNotificationsInteractor: PromotedNotificationsInteractor,
     dumpManager: DumpManager,
 ) : FlowDumperImpl(dumpManager) {
+    /** The content to show as the promoted notification on AOD */
     val content: Flow<PromotedNotificationContentModel?> =
-        activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs ->
-            notifs.firstNotNullOfOrNull { it.promotedContent }
-        }
+        promotedNotificationsInteractor.topPromotedNotificationContent
 
     val isPresent: Flow<Boolean> =
         content
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
new file mode 100644
index 0000000..1015cfb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2025 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.notification.promoted.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
+import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * An interactor that provides details about promoted notification precedence, based on the
+ * presented order of current notification status bar chips.
+ */
+@SysUISingleton
+class PromotedNotificationsInteractor
+@Inject
+constructor(
+    activeNotificationsInteractor: ActiveNotificationsInteractor,
+    callChipInteractor: CallChipInteractor,
+    notifChipsInteractor: StatusBarNotificationChipsInteractor,
+    @Background backgroundDispatcher: CoroutineDispatcher,
+) {
+    /**
+     * This is the ordered list of notifications (and the promoted content) represented as chips in
+     * the status bar.
+     */
+    private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> =
+        combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) {
+            callState,
+            notifChips ->
+            buildList {
+                val callData = callState.getNotifData()?.also { add(it) }
+                addAll(
+                    notifChips.mapNotNull {
+                        when (it.key) {
+                            callData?.key -> null // do not re-add the same call
+                            else -> NotifAndPromotedContent(it.key, it.promotedContent)
+                        }
+                    }
+                )
+            }
+        }
+
+    private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? =
+        when (this) {
+            is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent)
+            is OngoingCallModel.InCallWithVisibleApp ->
+                // TODO(b/395989259): support InCallWithVisibleApp when it has notif data
+                null
+            is OngoingCallModel.NoCall -> null
+        }
+
+    /**
+     * The top promoted notification represented by a chip, with the order determined by the order
+     * of the chips, not the notifications.
+     */
+    private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> =
+        orderedChipNotifications
+            .map { list -> list.firstNotNullOfOrNull { it.promotedContent } }
+            .distinctUntilNewInstance()
+
+    /** This is the top-most promoted notification, which should avoid regular changing. */
+    val topPromotedNotificationContent: Flow<PromotedNotificationContentModel?> =
+        combine(
+                topPromotedChipNotification,
+                activeNotificationsInteractor.topLevelRepresentativeNotifications,
+            ) { topChipNotif, topLevelNotifs ->
+                topChipNotif ?: topLevelNotifs.firstNotNullOfOrNull { it.promotedContent }
+            }
+            // #equals() can be a bit expensive on this object, but this flow will regularly try to
+            // emit the same immutable instance over and over, so just prevent that.
+            .distinctUntilNewInstance()
+
+    /**
+     * This is the ordered list of notifications (and the promoted content) represented as chips in
+     * the status bar. Flows on the background context.
+     */
+    val orderedChipNotificationKeys: Flow<List<String>> =
+        orderedChipNotifications
+            .map { list -> list.map { it.key } }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    /**
+     * Returns flow where all subsequent repetitions of the same object instance are filtered out.
+     */
+    private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b }
+
+    /**
+     * A custom pair, but providing clearer semantic names, and implementing equality as being the
+     * same instance of the promoted content model, which allows us to use distinctUntilChanged() on
+     * flows containing this without doing pixel comparisons on the Bitmaps inside Icon objects
+     * provided by the Notification.
+     */
+    private data class NotifAndPromotedContent(
+        val key: String,
+        val promotedContent: PromotedNotificationContentModel?,
+    ) {
+        /**
+         * Define the equals of this object to only check the reference equality of the promoted
+         * content so that we can mark.
+         */
+        override fun equals(other: Any?): Boolean {
+            return when {
+                other == null -> false
+                other === this -> true
+                other !is NotifAndPromotedContent -> return false
+                else -> key == other.key && promotedContent === other.promotedContent
+            }
+        }
+
+        /** Define the hashCode to be very quick, even if it increases collisions. */
+        override fun hashCode(): Int {
+            var result = key.hashCode()
+            result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0)
+            return result
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt
new file mode 100644
index 0000000..c3b2411
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2025 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.notification.row
+
+import android.content.Context
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.annotation.UiThread
+import com.android.app.tracing.coroutines.launchTraced
+import com.android.app.tracing.coroutines.withContextTraced
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.NotifInflation
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+
+@SysUISingleton
+class AsyncRowInflater
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainCoroutineDispatcher: CoroutineDispatcher,
+    @NotifInflation private val inflationCoroutineDispatcher: CoroutineDispatcher,
+) {
+    /**
+     * Inflate the layout on the background thread, and invoke the listener on the main thread when
+     * finished.
+     *
+     * If the inflation fails on the background, it will be retried once on the main thread.
+     */
+    @UiThread
+    fun inflate(
+        context: Context,
+        layoutFactory: LayoutInflater.Factory2,
+        @LayoutRes resId: Int,
+        parent: ViewGroup,
+        listener: OnInflateFinishedListener,
+    ): Job {
+        val inflater = BasicRowInflater(context).apply { factory2 = layoutFactory }
+        return applicationScope.launchTraced("AsyncRowInflater-bg", inflationCoroutineDispatcher) {
+            val view =
+                try {
+                    inflater.inflate(resId, parent, false)
+                } catch (ex: RuntimeException) {
+                    // Probably a Looper failure, retry on the UI thread
+                    Log.w(
+                        "AsyncRowInflater",
+                        "Failed to inflate resource in the background!" +
+                            " Retrying on the UI thread",
+                        ex,
+                    )
+                    null
+                }
+            withContextTraced("AsyncRowInflater-ui", mainCoroutineDispatcher) {
+                // If the inflate failed on the inflation thread, try again on the main thread
+                val finalView = view ?: inflater.inflate(resId, parent, false)
+                // Inform the listener of the completion
+                listener.onInflateFinished(finalView, resId, parent)
+            }
+        }
+    }
+
+    /**
+     * Callback interface (identical to the one from AsyncLayoutInflater) for receiving the inflated
+     * view
+     */
+    interface OnInflateFinishedListener {
+        @UiThread fun onInflateFinished(view: View, @LayoutRes resId: Int, parent: ViewGroup?)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt
new file mode 100644
index 0000000..79d50b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 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.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+
+/**
+ * A [LayoutInflater] that is copy of
+ * [androidx.asynclayoutinflater.view.AsyncLayoutInflater.BasicInflater]
+ */
+internal class BasicRowInflater(context: Context) : LayoutInflater(context) {
+    override fun cloneInContext(newContext: Context): LayoutInflater {
+        return BasicRowInflater(newContext)
+    }
+
+    @Throws(ClassNotFoundException::class)
+    override fun onCreateView(name: String, attrs: AttributeSet): View {
+        for (prefix in sClassPrefixList) {
+            try {
+                val view = createView(name, prefix, attrs)
+                if (view != null) {
+                    return view
+                }
+            } catch (e: ClassNotFoundException) {
+                // In this case we want to let the base class take a crack at it.
+            }
+        }
+
+        return super.onCreateView(name, attrs)
+    }
+
+    companion object {
+        private val sClassPrefixList = arrayOf("android.widget.", "android.webkit.", "android.app.")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index d1d1ea9..d383bee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -105,6 +105,7 @@
 import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -120,6 +121,7 @@
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
 import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
 import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
@@ -268,6 +270,7 @@
     private String mLoggingKey;
     private NotificationGuts mGuts;
     private NotificationEntry mEntry;
+    private EntryAdapter mEntryAdapter;
     private String mAppName;
     private NotificationRebindingTracker mRebindingTracker;
     private FalsingManager mFalsingManager;
@@ -390,11 +393,17 @@
     }
 
     private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) {
-        if (!shouldShowPublic() && (!mIsMinimized || isExpanded())
-                && mGroupMembershipManager.isGroupSummary(mEntry)) {
+        boolean isGroupRoot = NotificationBundleUi.isEnabled()
+                ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
+                : mGroupMembershipManager.isGroupSummary(mEntry);
+        if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) {
             mGroupExpansionChanging = true;
-            final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
-            boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry);
+            final boolean wasExpanded = NotificationBundleUi.isEnabled()
+                    ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter)
+                    : mGroupExpansionManager.isGroupExpanded(mEntry);
+            boolean nowExpanded = NotificationBundleUi.isEnabled()
+                    ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter)
+                    : mGroupExpansionManager.toggleGroupExpansion(mEntry);
             mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
             if (shouldLogExpandClickMetric) {
                 mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
@@ -910,6 +919,12 @@
         return mEntry;
     }
 
+    @Nullable
+    public EntryAdapter getEntryAdapter() {
+        NotificationBundleUi.assertInNewMode();
+        return mEntryAdapter;
+    }
+
     @Override
     public boolean isHeadsUp() {
         return mIsHeadsUp;
@@ -2010,11 +2025,25 @@
      *
      * @param context context context of the view
      * @param attrs   attributes used to initialize parent view
+     * @param user   the user the row is associated to
+     */
+    public ExpandableNotificationRow(Context context, AttributeSet attrs, UserHandle user) {
+        this(context, attrs, userContextForEntry(context, user));
+        NotificationBundleUi.assertInNewMode();
+    }
+
+    /**
+     * Constructs an ExpandableNotificationRow. Used by layout inflation (with a custom {@code
+     * AsyncLayoutFactory} in {@link RowInflaterTask}.
+     *
+     * @param context context context of the view
+     * @param attrs   attributes used to initialize parent view
      * @param entry   notification that the row will be associated to (determines the user for the
      *                ImageResolver)
      */
     public ExpandableNotificationRow(Context context, AttributeSet attrs, NotificationEntry entry) {
         this(context, attrs, userContextForEntry(context, entry));
+        NotificationBundleUi.assertInLegacyMode();
     }
 
     private static Context userContextForEntry(Context base, NotificationEntry entry) {
@@ -2025,6 +2054,13 @@
                 UserHandle.of(entry.getSbn().getNormalizedUserId()), /* flags= */ 0);
     }
 
+    private static Context userContextForEntry(Context base, UserHandle user) {
+        if (base.getUserId() == user.getIdentifier()) {
+            return base;
+        }
+        return base.createContextAsUser(user, /* flags= */ 0);
+    }
+
     private ExpandableNotificationRow(Context sysUiContext, AttributeSet attrs,
             Context userContext) {
         super(sysUiContext, attrs);
@@ -2067,7 +2103,14 @@
             IStatusBarService statusBarService,
             UiEventLogger uiEventLogger,
             NotificationRebindingTracker notificationRebindingTracker) {
-        mEntry = entry;
+
+        if (NotificationBundleUi.isEnabled()) {
+            // TODO (b/395857098): remove when all usages are migrated
+            mEntryAdapter = entry.getEntryAdapter();
+            mEntry = entry;
+        } else {
+            mEntry = entry;
+        }
         mAppName = appName;
         mRebindingTracker = notificationRebindingTracker;
         if (mMenuRow == null) {
@@ -2876,8 +2919,12 @@
     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
         if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion
                 && !mChildrenContainer.showingAsLowPriority()) {
-            final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
-            mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded);
+            final boolean wasExpanded = isGroupExpanded();
+            if (NotificationBundleUi.isEnabled()) {
+                mGroupExpansionManager.setGroupExpanded(mEntryAdapter, userExpanded);
+            } else {
+                mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded);
+            }
             onExpansionChanged(true /* userAction */, wasExpanded);
             return;
         }
@@ -3031,6 +3078,9 @@
 
     @Override
     public boolean isGroupExpanded() {
+        if (NotificationBundleUi.isEnabled()) {
+            return mGroupExpansionManager.isGroupExpanded(mEntryAdapter);
+        }
         return mGroupExpansionManager.isGroupExpanded(mEntry);
     }
 
@@ -3187,12 +3237,20 @@
     }
 
     public void setSensitive(boolean sensitive, boolean hideSensitive) {
+        if (notificationsRedesignTemplates()
+                && sensitive == mSensitive && hideSensitive == mSensitiveHiddenInGeneral) {
+            return; // nothing has changed
+        }
+
         int intrinsicBefore = getIntrinsicHeight();
         mSensitive = sensitive;
         mSensitiveHiddenInGeneral = hideSensitive;
         int intrinsicAfter = getIntrinsicHeight();
         if (intrinsicBefore != intrinsicAfter) {
             notifyHeightChanged(/* needsAnimation= */ true);
+        } else if (notificationsRedesignTemplates()) {
+            // Just request the correct layout, even if the height hasn't changed
+            getShowingLayout().requestSelectLayout(/* needsAnimation= */ true);
         }
     }
 
@@ -3227,11 +3285,14 @@
         }
         boolean oldShowingPublic = mShowingPublic;
         mShowingPublic = mSensitive && hideSensitive;
-        if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
+        boolean isShowingLayoutNotChanged = mShowingPublic == oldShowingPublic;
+        if (mShowingPublicInitialized && isShowingLayoutNotChanged) {
             return;
         }
 
-        if (!animated) {
+        final boolean shouldSkipHideSensitiveAnimation =
+                Flags.skipHideSensitiveNotifAnimation() && isShowingLayoutNotChanged;
+        if (!animated || shouldSkipHideSensitiveAnimation) {
             if (!NotificationContentAlphaOptimization.isEnabled()
                     || mShowingPublic != oldShowingPublic) {
                 // Don't reset the alpha or cancel the animation if the showing layout doesn't
@@ -3342,7 +3403,11 @@
     public void makeActionsVisibile() {
         setUserExpanded(true, true);
         if (isChildInGroup()) {
-            mGroupExpansionManager.setGroupExpanded(mEntry, true);
+            if (NotificationBundleUi.isEnabled()) {
+                mGroupExpansionManager.setGroupExpanded(mEntryAdapter, true);
+            } else {
+                mGroupExpansionManager.setGroupExpanded(mEntry, true);
+            }
         }
         notifyHeightChanged(/* needsAnimation= */ false);
     }
@@ -3766,7 +3831,9 @@
 
     public void onExpandedByGesture(boolean userExpanded) {
         int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER;
-        if (mGroupMembershipManager.isGroupSummary(mEntry)) {
+        if (NotificationBundleUi.isEnabled()
+                ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
+                : mGroupMembershipManager.isGroupSummary(mEntry)) {
             event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER;
         }
         mMetricsLogger.action(event, userExpanded);
@@ -3802,7 +3869,7 @@
     private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
         boolean nowExpanded = isExpanded();
         if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
-            nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
+            nowExpanded = isGroupExpanded();
         }
         // Note: nowExpanded is going to be true here on the first expansion of minimized groups,
         // even though the group itself is not expanded. Use mGroupExpansionManager to get the real
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index e311b53..0c1dd2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -56,6 +56,7 @@
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
@@ -1457,12 +1458,12 @@
         }
 
         @Override
-        public void handleInflationException(NotificationEntry entry, Exception e) {
+        public void handleInflationException(Exception e) {
             handleError(e);
         }
 
         @Override
-        public void onAsyncInflationFinished(NotificationEntry entry) {
+        public void onAsyncInflationFinished() {
             mEntry.onInflationTaskFinished();
             mRow.onNotificationUpdated();
             if (mCallback != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index 0be1d5d..05934e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 import java.lang.annotation.Retention;
@@ -170,13 +171,29 @@
          * @param entry notification which failed to inflate content
          * @param e exception
          */
-        void handleInflationException(NotificationEntry entry, Exception e);
+        default void handleInflationException(NotificationEntry entry, Exception e) {
+            handleInflationException(e);
+        }
+
+        /**
+         * Callback for when there is an inflation exception
+         *
+         * @param e exception
+         */
+        void handleInflationException(Exception e);
 
         /**
          * Callback for after the content views finish inflating.
          *
          * @param entry the entry with the content views set
          */
-        void onAsyncInflationFinished(NotificationEntry entry);
+        default void onAsyncInflationFinished(NotificationEntry entry) {
+            onAsyncInflationFinished();
+        }
+
+        /**
+         * Callback for after the content views finish inflating.
+         */
+        void onAsyncInflationFinished();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 517fc3a..761d3fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
 import com.android.systemui.statusbar.notification.InflationException
 import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
@@ -76,6 +77,7 @@
 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState
 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder
@@ -536,7 +538,7 @@
             val ident: String = (sbn.packageName + "/0x" + Integer.toHexString(sbn.id))
             Log.e(TAG, "couldn't inflate view for notification $ident", e)
             callback?.handleInflationException(
-                row.entry,
+                if (NotificationBundleUi.isEnabled) entry else row.entry,
                 InflationException("Couldn't inflate contentViews$e"),
             )
 
@@ -554,11 +556,11 @@
             logger.logAsyncTaskProgress(entry, "aborted")
         }
 
-        override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+        override fun handleInflationException(e: Exception) {
             handleError(e)
         }
 
-        override fun onAsyncInflationFinished(entry: NotificationEntry) {
+        override fun onAsyncInflationFinished() {
             this.entry.onInflationTaskFinished()
             row.onNotificationUpdated()
             callback?.onAsyncInflationFinished(this.entry)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index 6883ec5..da36140 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -21,10 +21,12 @@
 import androidx.annotation.NonNull;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import javax.inject.Inject;
 
@@ -52,7 +54,7 @@
 
     @Override
     protected void executeStage(
-            @NonNull NotificationEntry entry,
+            final @NonNull NotificationEntry entry,
             @NonNull ExpandableNotificationRow row,
             @NonNull StageCallback callback) {
         RowContentBindParams params = getStageParams(entry);
@@ -77,15 +79,35 @@
 
         InflationCallback inflationCallback = new InflationCallback() {
             @Override
-            public void handleInflationException(NotificationEntry entry, Exception e) {
-                mNotifInflationErrorManager.setInflationError(entry, e);
+            public void handleInflationException(NotificationEntry errorEntry, Exception e) {
+                if (NotificationBundleUi.isEnabled()) {
+                    mNotifInflationErrorManager.setInflationError(entry, e);
+                } else {
+                    mNotifInflationErrorManager.setInflationError(errorEntry, e);
+                }
             }
 
             @Override
-            public void onAsyncInflationFinished(NotificationEntry entry) {
-                mNotifInflationErrorManager.clearInflationError(entry);
-                getStageParams(entry).clearDirtyContentViews();
-                callback.onStageFinished(entry);
+            public void handleInflationException(Exception e) {
+
+            }
+
+            @Override
+            public void onAsyncInflationFinished(NotificationEntry finishedEntry) {
+                if (NotificationBundleUi.isEnabled()) {
+                    mNotifInflationErrorManager.clearInflationError(entry);
+                    getStageParams(entry).clearDirtyContentViews();
+                    callback.onStageFinished(entry);
+                } else {
+                    mNotifInflationErrorManager.clearInflationError(finishedEntry);
+                    getStageParams(finishedEntry).clearDirtyContentViews();
+                    callback.onStageFinished(finishedEntry);
+                }
+            }
+
+            @Override
+            public void onAsyncInflationFinished() {
+
             }
         };
         mBinder.cancelBind(entry, row);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
index 9f634be..3971661 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.row;
 
 import android.content.Context;
+import android.os.UserHandle;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -29,9 +30,12 @@
 import androidx.asynclayoutinflater.view.AsyncLayoutFactory;
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
 
+import com.android.systemui.Flags;
 import com.android.systemui.res.R;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.util.time.SystemClock;
 
 import java.util.concurrent.Executor;
@@ -41,7 +45,8 @@
 /**
  * An inflater task that asynchronously inflates a ExpandableNotificationRow
  */
-public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {
+public class RowInflaterTask implements InflationTask,
+        AsyncLayoutInflater.OnInflateFinishedListener, AsyncRowInflater.OnInflateFinishedListener {
 
     private static final String TAG = "RowInflaterTask";
     private static final boolean TRACE_ORIGIN = true;
@@ -52,12 +57,17 @@
     private Throwable mInflateOrigin;
     private final SystemClock mSystemClock;
     private final RowInflaterTaskLogger mLogger;
+    private final AsyncRowInflater mAsyncRowInflater;
     private long mInflateStartTimeMs;
+    private UserTracker mUserTracker;
 
     @Inject
-    public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) {
+    public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger,
+            UserTracker userTracker, AsyncRowInflater asyncRowInflater) {
         mSystemClock = systemClock;
         mLogger = logger;
+        mUserTracker = userTracker;
+        mAsyncRowInflater = asyncRowInflater;
     }
 
     /**
@@ -81,13 +91,19 @@
             mInflateOrigin = new Throwable("inflate requested here");
         }
         mListener = listener;
-        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry));
+        RowAsyncLayoutInflater asyncLayoutFactory = makeRowInflater(entry);
         mEntry = entry;
         entry.setInflationTask(this);
 
         mLogger.logInflateStart(entry);
         mInflateStartTimeMs = mSystemClock.elapsedRealtime();
-        inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this);
+        if (Flags.useNotifInflationThreadForRow()) {
+            mAsyncRowInflater.inflate(context, asyncLayoutFactory,
+                    R.layout.status_bar_notification_row, parent, this);
+        } else {
+            AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, asyncLayoutFactory);
+            inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this);
+        }
     }
 
     /**
@@ -107,40 +123,8 @@
     }
 
     private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) {
-        return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger);
-    }
-
-    /**
-     * A {@link LayoutInflater} that is copy of BasicLayoutInflater.
-     */
-    private static class BasicRowInflater extends LayoutInflater {
-        private static final String[] sClassPrefixList =
-                {"android.widget.", "android.webkit.", "android.app."};
-        BasicRowInflater(Context context) {
-            super(context);
-        }
-
-        @Override
-        public LayoutInflater cloneInContext(Context newContext) {
-            return new BasicRowInflater(newContext);
-        }
-
-        @Override
-        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
-            for (String prefix : sClassPrefixList) {
-                try {
-                    View view = createView(name, prefix, attrs);
-                    if (view != null) {
-                        return view;
-                    }
-                } catch (ClassNotFoundException e) {
-                    // In this case we want to let the base class take a crack
-                    // at it.
-                }
-            }
-
-            return super.onCreateView(name, attrs);
-        }
+        return new RowAsyncLayoutInflater(
+                entry, mSystemClock, mLogger, mUserTracker.getUserHandle());
     }
 
     @VisibleForTesting
@@ -148,12 +132,14 @@
         private final NotificationEntry mEntry;
         private final SystemClock mSystemClock;
         private final RowInflaterTaskLogger mLogger;
+        private final UserHandle mTargetUser;
 
         public RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock,
-                RowInflaterTaskLogger logger) {
+                RowInflaterTaskLogger logger, UserHandle targetUser) {
             mEntry = entry;
             mSystemClock = systemClock;
             mLogger = logger;
+            mTargetUser = targetUser;
         }
 
         @Nullable
@@ -165,8 +151,12 @@
             }
 
             final long startMs = mSystemClock.elapsedRealtime();
-            final ExpandableNotificationRow row =
-                    new ExpandableNotificationRow(context, attrs, mEntry);
+            ExpandableNotificationRow row = null;
+            if (NotificationBundleUi.isEnabled()) {
+                row = new ExpandableNotificationRow(context, attrs, mTargetUser);
+            } else {
+                row = new ExpandableNotificationRow(context, attrs, mEntry);
+            }
             final long elapsedMs = mSystemClock.elapsedRealtime() - startMs;
 
             mLogger.logCreatedRow(mEntry, elapsedMs);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index b9352bf..c694a19 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -122,6 +122,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
@@ -2958,9 +2959,13 @@
     }
 
     private boolean isChildInGroup(View child) {
-        return child instanceof ExpandableNotificationRow
-                && mGroupMembershipManager.isChildInGroup(
-                ((ExpandableNotificationRow) child).getEntry());
+        if (child instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow childRow = (ExpandableNotificationRow) child;
+            return NotificationBundleUi.isEnabled()
+                    ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter())
+                    : mGroupMembershipManager.isChildInGroup(childRow.getEntry());
+        }
+        return false;
     }
 
     /**
@@ -3639,6 +3644,9 @@
                     mScrollViewFields.sendCurrentGestureInGuts(false);
                     mScrollViewFields.sendCurrentGestureOverscroll(false);
                     setIsBeingDragged(false);
+                    // dispatch to touchHandlers, so they can still finalize a previously started
+                    // motion, while the shade is being dragged
+                    return super.dispatchTouchEvent(ev);
                 }
                 return false;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
index 832e690..78ece53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.stack.shared.model
 
+import androidx.compose.ui.geometry.Rect
+
 /** Models the bounds of the notification stack. */
 data class ShadeScrimBounds(
     /** The position of the left of the stack in its window coordinate system, in pixels. */
@@ -27,6 +29,10 @@
     /** The position of the bottom of the stack in its window coordinate system, in pixels. */
     val bottom: Float = 0f,
 ) {
+    constructor(
+        bounds: Rect
+    ) : this(left = bounds.left, top = bounds.top, right = bounds.right, bottom = bounds.bottom)
+
     /** The current height of the notification container. */
     val height: Float = bottom - top
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6385d53..10b665d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -25,7 +25,7 @@
 import com.android.internal.logging.nano.MetricsProto
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.NotifInflation
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
 import com.android.systemui.plugins.FalsingManager
@@ -76,7 +76,7 @@
 class NotificationListViewBinder
 @Inject
 constructor(
-    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @NotifInflation private val inflationDispatcher: CoroutineDispatcher,
     private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
     @ShadeDisplayAware private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
@@ -155,7 +155,7 @@
                 parentView,
                 attachToRoot = false,
             )
-            .flowOn(backgroundDispatcher)
+            .flowOn(inflationDispatcher)
             .collectLatest { footerView: FooterView ->
                 traceAsync("bind FooterView") {
                     parentView.setFooterView(footerView)
@@ -240,7 +240,7 @@
                 parentView,
                 attachToRoot = false,
             )
-            .flowOn(backgroundDispatcher)
+            .flowOn(inflationDispatcher)
             .collectLatest { emptyShadeView: EmptyShadeView ->
                 traceAsync("bind EmptyShadeView") {
                     parentView.setEmptyShadeView(emptyShadeView)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 2c8c7a1..54efa4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -48,7 +48,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
-import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
@@ -137,7 +136,6 @@
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
     private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel,
-    private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel,
     dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
@@ -574,7 +572,6 @@
             aodToLockscreenTransitionViewModel.notificationAlpha,
             aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
             aodToPrimaryBouncerTransitionViewModel.notificationAlpha,
-            dozingToDreamingTransitionViewModel.notificationAlpha,
             dozingToLockscreenTransitionViewModel.lockscreenAlpha,
             dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
             dozingToPrimaryBouncerTransitionViewModel.notificationAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 1dc9de4..05a46cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -54,6 +54,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.kotlin.JavaAdapter;
@@ -215,7 +216,11 @@
             if (ExpandHeadsUpOnInlineReply.isEnabled()) {
                 if (row.isChildInGroup() && !row.areChildrenExpanded()) {
                     // The group isn't expanded, let's make sure it's visible!
-                    mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+                    if (NotificationBundleUi.isEnabled()) {
+                        mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter());
+                    } else {
+                        mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+                    }
                 } else if (!row.isChildInGroup()) {
                     final boolean expandNotification;
                     if (row.isPinned()) {
@@ -233,7 +238,11 @@
             } else {
                 if (row.isChildInGroup() && !row.areChildrenExpanded()) {
                     // The group isn't expanded, let's make sure it's visible!
-                    mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+                    if (NotificationBundleUi.isEnabled()) {
+                        mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter());
+                    } else {
+                        mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+                    }
                 }
 
                 if (android.app.Flags.compactHeadsUpNotificationReply()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 788f041..0eef2e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_ACTIVE
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_INACTIVE
+import com.android.systemui.util.kotlin.pairwiseBy
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -131,19 +132,37 @@
 
                     // Set the icon for the triangle
                     launch {
-                        viewModel.icon.distinctUntilChanged().collect { icon ->
-                            viewModel.verboseLogger?.logBinderReceivedSignalIcon(
-                                view,
-                                viewModel.subscriptionId,
-                                icon,
-                            )
-                            if (icon is SignalIconModel.Cellular) {
-                                iconView.setImageDrawable(mobileDrawable)
-                                mobileDrawable.level = icon.toSignalDrawableState()
-                            } else if (icon is SignalIconModel.Satellite) {
-                                IconViewBinder.bind(icon.icon, iconView)
+                        viewModel.icon
+                            .pairwiseBy(initialValue = null) { oldIcon, newIcon ->
+                                // Make sure we requestLayout if the number of levels changes
+                                val shouldRequestLayout =
+                                    when {
+                                        oldIcon == null -> true
+                                        oldIcon is SignalIconModel.Cellular &&
+                                            newIcon is SignalIconModel.Cellular -> {
+                                            oldIcon.numberOfLevels != newIcon.numberOfLevels
+                                        }
+                                        else -> false
+                                    }
+                                Pair(shouldRequestLayout, newIcon)
                             }
-                        }
+                            .collect { (shouldRequestLayout, newIcon) ->
+                                viewModel.verboseLogger?.logBinderReceivedSignalIcon(
+                                    view,
+                                    viewModel.subscriptionId,
+                                    newIcon,
+                                )
+                                if (newIcon is SignalIconModel.Cellular) {
+                                    iconView.setImageDrawable(mobileDrawable)
+                                    mobileDrawable.level = newIcon.toSignalDrawableState()
+                                } else if (newIcon is SignalIconModel.Satellite) {
+                                    IconViewBinder.bind(newIcon.icon, iconView)
+                                }
+
+                                if (shouldRequestLayout) {
+                                    iconView.requestLayout()
+                                }
+                            }
                     }
 
                     launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index fd5ab13..7eda87f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -19,19 +19,18 @@
 import android.content.Context
 import android.util.AttributeSet
 import android.view.LayoutInflater
+import android.widget.FrameLayout
 import android.widget.ImageView
-import com.android.settingslib.flags.Flags.newStatusBarIcons
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString
+import com.android.systemui.statusbar.core.NewStatusBarIcons
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
 import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
 
-class ModernStatusBarMobileView(
-    context: Context,
-    attrs: AttributeSet?,
-) : ModernStatusBarView(context, attrs) {
+class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) :
+    ModernStatusBarView(context, attrs) {
 
     var subId: Int = -1
 
@@ -62,15 +61,34 @@
                     as ModernStatusBarMobileView)
                 .also {
                     // Flag-specific configuration
-                    if (newStatusBarIcons()) {
-                        // New icon (with no embedded whitespace) is slightly shorter
-                        // (but actually taller)
-                        val iconView = it.requireViewById<ImageView>(R.id.mobile_signal)
-                        val lp = iconView.layoutParams
-                        lp.height =
-                            context.resources.getDimensionPixelSize(
-                                R.dimen.status_bar_mobile_signal_size_updated
-                            )
+                    if (NewStatusBarIcons.isEnabled) {
+                        // triangle
+                        it.requireViewById<ImageView>(R.id.mobile_signal).apply {
+                            layoutParams.height =
+                                context.resources.getDimensionPixelSize(
+                                    R.dimen.status_bar_mobile_signal_size_updated
+                                )
+                        }
+
+                        // RAT indicator container
+                        it.requireViewById<FrameLayout>(R.id.mobile_type_container).apply {
+                            (layoutParams as MarginLayoutParams).marginEnd =
+                                context.resources.getDimensionPixelSize(
+                                    R.dimen.status_bar_mobile_container_margin_end
+                                )
+                            layoutParams.height =
+                                context.resources.getDimensionPixelSize(
+                                    R.dimen.status_bar_mobile_container_height_updated
+                                )
+                        }
+
+                        // RAT indicator
+                        it.requireViewById<ImageView>(R.id.mobile_type).apply {
+                            layoutParams.height =
+                                context.resources.getDimensionPixelSize(
+                                    R.dimen.status_bar_mobile_type_size_updated
+                                )
+                        }
                     }
 
                     it.subId = viewModel.subscriptionId
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 171e4f5..e37c3f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.flags.Flags.NEW_NETWORK_SLICE_UI
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.NewStatusBarIcons
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
@@ -278,10 +279,11 @@
                 flowOf(null)
             } else {
                 iconInteractor.showSliceAttribution.map {
-                    if (it) {
-                        Icon.Resource(R.drawable.mobile_network_type_background, null)
-                    } else {
-                        null
+                    when {
+                        it && NewStatusBarIcons.isEnabled ->
+                            Icon.Resource(R.drawable.mobile_network_type_background_updated, null)
+                        it -> Icon.Resource(R.drawable.mobile_network_type_background, null)
+                        else -> null
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index a961713..39a1b46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -255,10 +255,9 @@
                                 )
 
                                 setContent {
-                                    val chips =
-                                        statusBarViewModel.statusBarPopupChips
-                                            .collectAsStateWithLifecycle()
-                                    StatusBarPopupChipsContainer(chips = chips.value)
+                                    StatusBarPopupChipsContainer(
+                                        chips = statusBarViewModel.popupChips
+                                    )
                                 }
                             }
                         endSideContent.addView(composeView, 0)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index f396cbf..9ae2cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -20,6 +20,7 @@
 import android.graphics.Rect
 import android.view.View
 import androidx.compose.runtime.getValue
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -29,6 +30,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
 import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.lifecycle.Activatable
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.log.table.TableLogBufferFactory
@@ -49,6 +51,7 @@
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
 import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
+import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
 import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel
 import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior
@@ -71,6 +74,8 @@
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -94,7 +99,7 @@
  * [StatusBarHideIconsForBouncerManager]. We should move those pieces of logic to this class instead
  * so that it's all in one place and easily testable outside of the fragment.
  */
-interface HomeStatusBarViewModel {
+interface HomeStatusBarViewModel : Activatable {
     /** Factory to create the view model for the battery icon */
     val batteryViewModelFactory: BatteryViewModel.Factory
 
@@ -133,7 +138,7 @@
     val operatorNameViewModel: StatusBarOperatorNameViewModel
 
     /** The popup chips that should be shown on the right-hand side of the status bar. */
-    val statusBarPopupChips: StateFlow<List<PopupChipModel.Shown>>
+    val popupChips: List<PopupChipModel.Shown>
 
     /**
      * True if the current scene can show the home status bar (aka this status bar), and false if
@@ -208,7 +213,7 @@
     shadeInteractor: ShadeInteractor,
     shareToAppChipViewModel: ShareToAppChipViewModel,
     ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
-    statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel,
+    statusBarPopupChipsViewModelFactory: StatusBarPopupChipsViewModel.Factory,
     animations: SystemStatusEventAnimationInteractor,
     statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
     @Background bgScope: CoroutineScope,
@@ -219,6 +224,8 @@
 
     val tableLogger = tableLoggerFactory.getOrCreate(tableLogBufferName(thisDisplayId), 200)
 
+    private val statusBarPopupChips by lazy { statusBarPopupChipsViewModelFactory.create() }
+
     override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
         keyguardTransitionInteractor
             .isInTransition(Edge.create(from = LOCKSCREEN, to = OCCLUDED))
@@ -246,7 +253,8 @@
 
     override val ongoingActivityChipsLegacy = ongoingActivityChipsViewModel.chipsLegacy
 
-    override val statusBarPopupChips = statusBarPopupChipsViewModel.shownPopupChips
+    override val popupChips
+        get() = statusBarPopupChips.shownPopupChips
 
     override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> =
         combine(
@@ -495,7 +503,13 @@
     private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE
 
     override suspend fun onActivated(): Nothing {
-        hydrator.activate()
+        coroutineScope {
+            launch { hydrator.activate() }
+            if (StatusBarPopupChips.isEnabled) {
+                launch { statusBarPopupChips.activate() }
+            }
+            awaitCancellation()
+        }
     }
 
     /** Inject this to create the display-dependent view model */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index 9ad8619..1d1826d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.policy;
 
 import android.app.AlarmManager;
-import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -175,11 +174,7 @@
 
     @Override
     public void setZen(int zen, Uri conditionId, String reason) {
-        if (Flags.modesApi()) {
-            mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
-        } else {
-            mNoMan.setZenMode(zen, conditionId, reason);
-        }
+        mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 28cf78f..9f60fe21 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -18,8 +18,10 @@
 
 import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
 
+import static com.android.systemui.Flags.hardwareColorStyles;
 import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
@@ -73,6 +75,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -99,6 +102,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -136,9 +140,11 @@
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final Resources mResources;
     // Current wallpaper colors associated to a user.
-    private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
+    @VisibleForTesting
+    protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
     private final WallpaperManager mWallpaperManager;
     private final ActivityManager mActivityManager;
+    protected final SystemPropertiesHelper mSystemPropertiesHelper;
     @VisibleForTesting
     protected ColorScheme mColorScheme;
     // If fabricated overlays were already created for the current theme.
@@ -423,7 +429,9 @@
             JavaAdapter javaAdapter,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             UiModeManager uiModeManager,
-            ActivityManager activityManager) {
+            ActivityManager activityManager,
+            SystemPropertiesHelper systemPropertiesHelper
+    ) {
         mContext = context;
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
@@ -443,6 +451,7 @@
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mUiModeManager = uiModeManager;
         mActivityManager = activityManager;
+        mSystemPropertiesHelper = systemPropertiesHelper;
         dumpManager.registerDumpable(TAG, this);
 
         Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor
@@ -498,29 +507,38 @@
         mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
 
+        WallpaperColors systemColor;
+        if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) {
+            Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults();
+            mThemeStyle = defaultSettings.first;
+            Color seedColor = defaultSettings.second;
+
+            // we only use the first color anyway, so we can pass only the single color we have
+            systemColor = new WallpaperColors(
+                    /*primaryColor*/ seedColor,
+                    /*secondaryColor*/ seedColor,
+                    /*tertiaryColor*/ seedColor
+            );
+        } else {
+            systemColor = mWallpaperManager.getWallpaperColors(
+                    getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
+        }
+
         // Upon boot, make sure we have the most up to date colors
         Runnable updateColors = () -> {
-            WallpaperColors systemColor = mWallpaperManager.getWallpaperColors(
-                    getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
-            Runnable applyColors = () -> {
-                if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
-                mCurrentColors.put(mUserTracker.getUserId(), systemColor);
-                reevaluateSystemTheme(false /* forceReload */);
-            };
-            if (mDeviceProvisionedController.isCurrentUserSetup()) {
-                mMainExecutor.execute(applyColors);
-            } else {
-                applyColors.run();
-            }
+            if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
+            mCurrentColors.put(mUserTracker.getUserId(), systemColor);
+            reevaluateSystemTheme(false /* forceReload */);
         };
 
         // Whenever we're going directly to setup wizard, we need to process colors synchronously,
         // otherwise we'll see some jank when the activity is recreated.
         if (!mDeviceProvisionedController.isCurrentUserSetup()) {
-            updateColors.run();
+            mMainExecutor.execute(updateColors);
         } else {
             mBgExecutor.execute(updateColors);
         }
+
         mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null,
                 UserHandle.USER_ALL);
 
@@ -604,7 +622,7 @@
 
     @VisibleForTesting
     protected boolean isPrivateProfile(UserHandle userHandle) {
-        Context usercontext = mContext.createContextAsUser(userHandle,0);
+        Context usercontext = mContext.createContextAsUser(userHandle, 0);
         return usercontext.getSystemService(UserManager.class).isPrivateProfile();
     }
 
@@ -720,6 +738,7 @@
         return true;
     }
 
+    @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized
     private void updateThemeOverlays() {
         final int currentUser = mUserTracker.getUserId();
         final String overlayPackageJson = mSecureSettings.getStringForUser(
@@ -746,7 +765,7 @@
         OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE);
         if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) {
             try {
-                String colorString =  systemPalette.getPackageName().toLowerCase();
+                String colorString = systemPalette.getPackageName().toLowerCase();
                 if (!colorString.startsWith("#")) {
                     colorString = "#" + colorString;
                 }
@@ -856,6 +875,75 @@
         return style;
     }
 
+    protected Pair<Integer, String> getHardwareColorSetting() {
+        String deviceColorProperty = "ro.boot.hardware.color";
+
+        String[] themeData = mResources.getStringArray(
+                com.android.internal.R.array.theming_defaults);
+
+        // Color can be hex (`#FF0000`) or `home_wallpaper`
+        Map<String, Pair<Integer, String>> themeMap = new HashMap<>();
+
+        // extract all theme settings
+        for (String themeEntry : themeData) {
+            String[] themeComponents = themeEntry.split("\\|");
+            if (themeComponents.length != 3) continue;
+            themeMap.put(themeComponents[0],
+                    new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2]));
+        }
+
+        Pair<Integer, String> fallbackTheme = themeMap.get("*");
+        if (fallbackTheme == null) {
+            Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME);
+            fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME);
+        }
+
+        String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty);
+        Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue);
+        if (selectedTheme == null) {
+            Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue
+                    + "' not found in theming_defaults: " + Arrays.toString(themeData));
+            selectedTheme = fallbackTheme;
+        }
+
+        return selectedTheme;
+    }
+
+    @VisibleForTesting
+    protected Pair<Integer, Color> getThemeSettingsDefaults() {
+
+        Pair<Integer, String> selectedTheme = getHardwareColorSetting();
+
+        // Last fallback color
+        Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE);
+
+        // defaultColor will come from wallpaper or be parsed from a string
+        boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME);
+
+        if (isWallpaper) {
+            WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors(
+                    getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
+
+            if (wallpaperColors != null) {
+                defaultSeedColor = wallpaperColors.getPrimaryColor();
+            }
+
+            Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString(
+                    defaultSeedColor.toArgb()));
+        } else {
+            try {
+                defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second));
+                Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString(
+                        defaultSeedColor.toArgb()));
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Error parsing color: " + selectedTheme.second, e);
+                // defaultSeedColor remains unchanged in this case
+            }
+        }
+
+        return new Pair<>(selectedTheme.first, defaultSeedColor);
+    }
+
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("mSystemColors=" + mCurrentColors);
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
index e5c1e7d..79ff38e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.NotifInflation
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.util.settings.SettingsSingleThreadBackground
 import dagger.Module
@@ -123,4 +124,19 @@
     ): CoroutineContext {
         return uiBgCoroutineDispatcher
     }
+
+    /** Coroutine dispatcher for background notification inflation. */
+    @Provides
+    @NotifInflation
+    @SysUISingleton
+    fun notifInflationCoroutineDispatcher(
+        @NotifInflation notifInflationExecutor: Executor,
+        @Background bgCoroutineDispatcher: CoroutineDispatcher,
+    ): CoroutineDispatcher {
+        if (com.android.systemui.Flags.useNotifInflationThreadForFooter()) {
+            return notifInflationExecutor.asCoroutineDispatcher()
+        } else {
+            return bgCoroutineDispatcher
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt
new file mode 100644
index 0000000..4d6eb4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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.util.settings.repository
+
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SecureSettings
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Repository observing values of a [Settings.Secure] for the specified user. */
+@SysUISingleton
+class SecureSettingsForUserRepository
+@Inject
+constructor(
+    secureSettings: SecureSettings,
+    @Background backgroundDispatcher: CoroutineDispatcher,
+    @Background backgroundContext: CoroutineContext,
+) : SettingsForUserRepository(secureSettings, backgroundDispatcher, backgroundContext)
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt
new file mode 100644
index 0000000..94b3fd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2025 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.util.settings.repository
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.util.settings.UserSettingsProxy
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository observing values of a [UserSettingsProxy] for the specified user. This repository
+ * should be used for any system that tracks the desired user internally (e.g. the Quick Settings
+ * tiles system). In other cases, use a [UserAwareSettingsRepository] instead.
+ */
+abstract class SettingsForUserRepository(
+    private val userSettings: UserSettingsProxy,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Background private val backgroundContext: CoroutineContext,
+) {
+    fun boolSettingForUser(
+        userId: Int,
+        name: String,
+        defaultValue: Boolean = false,
+    ): Flow<Boolean> =
+        settingObserver(name, userId) { userSettings.getBoolForUser(name, defaultValue, userId) }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> {
+        return userSettings
+            .observerFlow(userId, name)
+            .onStart { emit(Unit) }
+            .map { settingsReader.invoke() }
+    }
+
+    suspend fun setBoolForUser(userId: Int, name: String, value: Boolean) {
+        withContext(backgroundContext) { userSettings.putBoolForUser(name, value, userId) }
+    }
+
+    suspend fun getBoolForUser(userId: Int, name: String, defaultValue: Boolean = false): Boolean {
+        return withContext(backgroundContext) {
+            userSettings.getBoolForUser(name, defaultValue, userId)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
index 73329b4..a8068cd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
@@ -33,7 +33,8 @@
 /**
  * Repository for observing values of a [UserSettingsProxy], for the currently active user. That
  * means that when the user is switched and the new user has a different value, the flow will emit
- * the new value.
+ * the new value. For any system that tracks the desired user internally (e.g. the Quick Settings
+ * tiles system), use a [SettingsForUserRepository] instead.
  */
 // TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal.
 abstract class UserAwareSettingsRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index ae3756d..6fc9561 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -308,9 +308,9 @@
     private int mWindowGravity;
 
     @VisibleForTesting
-    final int mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
+    final int mVolumeRingerIconDrawableId = R.drawable.ic_legacy_speaker_on;
     @VisibleForTesting
-    final int mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
+    final int mVolumeRingerMuteIconDrawableId = R.drawable.ic_legacy_speaker_mute;
 
     private int mOriginalGravity;
     private final DevicePostureController.Callback mDevicePostureControllerCallback;
@@ -1791,8 +1791,9 @@
             enableRingerViewsH(!isZenMuted);
             switch (mState.ringerModeInternal) {
                 case AudioManager.RINGER_MODE_VIBRATE:
-                    mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
-                    mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
+                    mRingerIcon.setImageResource(R.drawable.ic_legacy_volume_ringer_vibrate);
+                    mSelectedRingerIcon.setImageResource(
+                            R.drawable.ic_legacy_volume_ringer_vibrate);
                     addAccessibilityDescription(mRingerIcon, RINGER_MODE_VIBRATE,
                             mContext.getString(R.string.volume_ringer_hint_mute));
                     mRingerIcon.setTag(Events.ICON_STATE_VIBRATE);
@@ -1990,7 +1991,7 @@
         if (zenMuted) {
             iconRes = com.android.internal.R.drawable.ic_qs_dnd;
         } else if (isRingVibrate) {
-            iconRes = R.drawable.ic_volume_ringer_vibrate;
+            iconRes = R.drawable.ic_legacy_volume_ringer_vibrate;
         } else if (isRingSilent) {
             iconRes = row.iconMuteRes;
         } else if (ss.routedToBluetooth) {
@@ -2009,7 +2010,7 @@
 
         row.setIcon(iconRes, mContext.getTheme());
         row.iconState =
-                iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE
+                iconRes == R.drawable.ic_legacy_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE
                 : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes)
                         ? Events.ICON_STATE_MUTE
                 : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index ef75057..5ef0319 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -107,13 +107,6 @@
                     fullRadius = volumeDialogBgFullRadius,
                     diff = volumeDialogBgFullRadius - volumeDialogBgSmallRadius,
                     progress,
-                    isBottom = false,
-                )
-                volumeDialogBackgroundView.applyCorners(
-                    fullRadius = volumeDialogBgFullRadius,
-                    diff = volumeDialogBgFullRadius - volumeDialogBgSmallRadius,
-                    progress,
-                    isBottom = true,
                 )
             }
         val ringerDrawerTransitionListener = VolumeDialogRingerDrawerTransitionListener {
@@ -132,10 +125,10 @@
 
                         // Set up view background and visibility
                         drawerContainer.visibility = View.VISIBLE
+                        (volumeDialogBackgroundView.background as GradientDrawable).cornerRadii =
+                            bottomCornerRadii
                         when (uiModel.drawerState) {
                             is RingerDrawerState.Initial -> {
-                                (volumeDialogBackgroundView.background as GradientDrawable)
-                                    .cornerRadii = bottomCornerRadii
                                 drawerContainer.animateAndBindDrawerButtons(
                                     viewModel,
                                     uiModel,
@@ -216,8 +209,6 @@
                                 drawerContainer.transitionToState(
                                     R.id.volume_dialog_ringer_drawer_open
                                 )
-                                volumeDialogBackgroundView.background =
-                                    volumeDialogBackgroundView.background.mutate()
                                 ringerBackgroundView.background =
                                     ringerBackgroundView.background.mutate()
                             }
@@ -423,14 +414,9 @@
         }
     }
 
-    private fun View.applyCorners(fullRadius: Int, diff: Int, progress: Float, isBottom: Boolean) {
+    private fun View.applyCorners(fullRadius: Int, diff: Int, progress: Float) {
         val radius = fullRadius - progress * diff
-        (background as GradientDrawable).cornerRadii =
-            if (isBottom) {
-                floatArrayOf(0F, 0F, 0F, 0F, radius, radius, radius, radius)
-            } else {
-                FloatArray(8) { radius }
-            }
+        (background as GradientDrawable).cornerRadius = radius
         background.invalidateSelf()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index e2d2f3f..3efb2b4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
 
-import android.content.Context
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
@@ -24,6 +23,7 @@
 import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
 import com.android.systemui.res.R
 import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -34,6 +34,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -43,21 +44,25 @@
 @AssistedInject
 constructor(
     @Assisted private val coroutineScope: CoroutineScope,
-    private val context: Context,
     private val audioSharingInteractor: AudioSharingInteractor,
     private val uiEventLogger: UiEventLogger,
     private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+    private val volumePanelLogger: VolumePanelLogger,
 ) : SliderViewModel {
     private val volumeChanges = MutableStateFlow<Int?>(null)
 
     override val slider: StateFlow<SliderState> =
-        combine(audioSharingInteractor.volume, audioSharingInteractor.secondaryDevice) {
-                volume,
-                device ->
+        combine(
+                audioSharingInteractor.volume.distinctUntilChanged().onEach {
+                    it?.let(volumePanelLogger::onAudioSharingVolumeUpdateReceived)
+                },
+                audioSharingInteractor.secondaryDevice,
+            ) { volume, device ->
                 val deviceName = device?.name ?: return@combine SliderState.Empty
                 if (volume == null) {
                     SliderState.Empty
                 } else {
+
                     State(
                         value = volume.toFloat(),
                         valueRange =
@@ -74,13 +79,15 @@
     init {
         volumeChanges
             .filterNotNull()
-            .onEach { audioSharingInteractor.setStreamVolume(it) }
+            .onEach {
+                volumePanelLogger.onSetAudioSharingVolumeRequested(it)
+                audioSharingInteractor.setStreamVolume(it)
+            }
             .launchIn(coroutineScope)
     }
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
-        val audioViewModel = state as? State
-        audioViewModel ?: return
+        if (state !is State) return
         volumeChanges.tryEmit(newValue.roundToInt())
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 5332764..d74a433 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -44,17 +45,23 @@
     private val context: Context,
     private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+    private val volumePanelLogger: VolumePanelLogger,
 ) : SliderViewModel {
 
     override val slider: StateFlow<SliderState> =
         mediaDeviceSessionInteractor
             .playbackInfo(session)
-            .mapNotNull { it?.getCurrentState() }
+            .mapNotNull {
+                volumePanelLogger.onVolumeUpdateReceived(session.sessionToken, it.currentVolume)
+                it.getCurrentState()
+            }
             .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
         coroutineScope.launch {
-            mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
+            val volume = newValue.roundToInt()
+            volumePanelLogger.onSetVolumeRequested(session.sessionToken, volume)
+            mediaDeviceSessionInteractor.setSessionVolume(session, volume)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
index 276326c..930199a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.volume.panel.shared
 
+import android.media.session.MediaSession
+import android.media.session.MediaSession.Token
 import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
@@ -42,7 +44,7 @@
                 str1 = key
                 bool1 = isAvailable
             },
-            { "$str1 isAvailable=$bool1" }
+            { "$str1 isAvailable=$bool1" },
         )
     }
 
@@ -51,7 +53,7 @@
             TAG,
             LogLevel.DEBUG,
             { bool1 = globalState.isVisible },
-            { "Global state changed: isVisible=$bool1" }
+            { "Global state changed: isVisible=$bool1" },
         )
     }
 
@@ -63,7 +65,7 @@
                 str1 = audioStream.toString()
                 int1 = volume
             },
-            { "Set volume: stream=$str1 volume=$int1" }
+            { "Set volume: stream=$str1 volume=$int1" },
         )
     }
 
@@ -75,7 +77,49 @@
                 str1 = audioStream.toString()
                 int1 = volume
             },
-            { "Volume update received: stream=$str1 volume=$int1" }
+            { "Volume update received: stream=$str1 volume=$int1" },
+        )
+    }
+
+    fun onSetVolumeRequested(sessionToken: MediaSession.Token, volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = sessionToken.toString()
+                int1 = volume
+            },
+            { "Set volume: token=$str1 volume=$int1" },
+        )
+    }
+
+    fun onVolumeUpdateReceived(sessionToken: Token, volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = sessionToken.toString()
+                int1 = volume
+            },
+            { "Volume update received: token=$str1 volume=$int1" },
+        )
+    }
+
+    fun onSetAudioSharingVolumeRequested(volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = volume },
+            { "Set volume: audio-sharing volume=$int1" },
+        )
+    }
+
+    fun onAudioSharingVolumeUpdateReceived(volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = volume },
+            { "Volume update received: audio-sharing volume=$int1" },
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index ec74f4f..300a7e0 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.wallpapers.data.repository
 
 import android.app.WallpaperInfo
+import android.graphics.PointF
+import android.graphics.RectF
 import android.view.View
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
@@ -37,4 +39,8 @@
     override val wallpaperSupportsAmbientMode = flowOf(false)
     override var rootView: View? = null
     override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+    override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {}
+
+    override fun sendTapCommand(tapPosition: PointF) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
index 2c3491b..974468c 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
@@ -33,7 +33,8 @@
 
     val wallpaperFocalAreaBounds: StateFlow<RectF>
 
-    val wallpaperFocalAreaTapPosition: StateFlow<PointF>
+    /** It will be true when wallpaper requires focal area info. */
+    val hasFocalArea: StateFlow<Boolean>
 
     /** top of notifications without bcsmartspace in small clock settings */
     val notificationDefaultTop: StateFlow<Float>
@@ -51,7 +52,9 @@
 }
 
 @SysUISingleton
-class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAreaRepository {
+class WallpaperFocalAreaRepositoryImpl
+@Inject
+constructor(val wallpaperRepository: WallpaperRepository) : WallpaperFocalAreaRepository {
 
     private val _shortcutAbsoluteTop = MutableStateFlow(0F)
     override val shortcutAbsoluteTop = _shortcutAbsoluteTop.asStateFlow()
@@ -63,13 +66,11 @@
     override val wallpaperFocalAreaBounds: StateFlow<RectF> =
         _wallpaperFocalAreaBounds.asStateFlow()
 
-    private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F))
-    override val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
-        _wallpaperFocalAreaTapPosition.asStateFlow()
-
     private val _notificationDefaultTop = MutableStateFlow(0F)
     override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow()
 
+    override val hasFocalArea = wallpaperRepository.shouldSendFocalArea
+
     override fun setShortcutAbsoluteTop(top: Float) {
         _shortcutAbsoluteTop.value = top
     }
@@ -84,9 +85,10 @@
 
     override fun setWallpaperFocalAreaBounds(bounds: RectF) {
         _wallpaperFocalAreaBounds.value = bounds
+        wallpaperRepository.sendLockScreenLayoutChangeCommand(bounds)
     }
 
-    override fun setTapPosition(point: PointF) {
-        _wallpaperFocalAreaTapPosition.value = point
+    override fun setTapPosition(tapPosition: PointF) {
+        wallpaperRepository.sendTapCommand(tapPosition)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index a55f76b..b07342c 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -21,22 +21,18 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.graphics.PointF
+import android.graphics.RectF
 import android.os.Bundle
 import android.os.UserHandle
 import android.provider.Settings
+import android.util.Log
 import android.view.View
-import androidx.annotation.VisibleForTesting
-import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.Edge
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.res.R as SysUIR
-import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shared.Flags.ambientAod
 import com.android.systemui.shared.Flags.extendedWallpaperEffects
 import com.android.systemui.user.data.model.SelectedUserModel
@@ -48,7 +44,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -76,6 +71,10 @@
 
     /** some wallpapers require bounds to be sent from keyguard */
     val shouldSendFocalArea: StateFlow<Boolean>
+
+    fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF)
+
+    fun sendTapCommand(tapPosition: PointF)
 }
 
 @SysUISingleton
@@ -86,10 +85,8 @@
     @Background private val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     userRepository: UserRepository,
-    wallpaperFocalAreaRepository: WallpaperFocalAreaRepository,
     private val wallpaperManager: WallpaperManager,
     private val context: Context,
-    keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val secureSettings: SecureSettings,
 ) : WallpaperRepository {
     private val wallpaperChanged: Flow<Unit> =
@@ -109,9 +106,6 @@
             // Only update the wallpaper status once the user selection has finished.
             .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
 
-    @VisibleForTesting var sendLockscreenLayoutJob: Job? = null
-    @VisibleForTesting var sendTapInShapeEffectsJob: Job? = null
-
     override val wallpaperInfo: StateFlow<WallpaperInfo?> =
         if (!wallpaperManager.isWallpaperSupported) {
             MutableStateFlow(null).asStateFlow()
@@ -143,77 +137,45 @@
 
     override var rootView: View? = null
 
+    override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {
+        if (DEBUG) {
+            Log.d(TAG, "sendLockScreenLayoutChangeCommand $wallpaperFocalAreaBounds")
+        }
+        wallpaperManager.sendWallpaperCommand(
+            /* windowToken = */ rootView?.windowToken,
+            /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_LAYOUT_CHANGED,
+            /* x = */ 0,
+            /* y = */ 0,
+            /* z = */ 0,
+            /* extras = */ Bundle().apply {
+                putFloat("wallpaperFocalAreaLeft", wallpaperFocalAreaBounds.left)
+                putFloat("wallpaperFocalAreaRight", wallpaperFocalAreaBounds.right)
+                putFloat("wallpaperFocalAreaTop", wallpaperFocalAreaBounds.top)
+                putFloat("wallpaperFocalAreaBottom", wallpaperFocalAreaBounds.bottom)
+            },
+        )
+    }
+
+    override fun sendTapCommand(tapPosition: PointF) {
+        if (DEBUG) {
+            Log.d(TAG, "sendTapCommand $tapPosition")
+        }
+
+        wallpaperManager.sendWallpaperCommand(
+            /* windowToken = */ rootView?.windowToken,
+            /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP,
+            /* x = */ tapPosition.x.toInt(),
+            /* y = */ tapPosition.y.toInt(),
+            /* z = */ 0,
+            /* extras = */ Bundle(),
+        )
+    }
+
     override val shouldSendFocalArea =
         wallpaperInfo
             .map {
                 val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target)
                 val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget
-                if (shouldSendNotificationLayout) {
-                    sendLockscreenLayoutJob =
-                        scope.launch {
-                            combine(
-                                    wallpaperFocalAreaRepository.wallpaperFocalAreaBounds,
-                                    keyguardTransitionInteractor
-                                        .transition(
-                                            edge = Edge.create(to = Scenes.Lockscreen),
-                                            edgeWithoutSceneContainer =
-                                                Edge.create(to = KeyguardState.LOCKSCREEN),
-                                        )
-                                        .filter { transitionStep ->
-                                            transitionStep.transitionState ==
-                                                TransitionState.STARTED
-                                        },
-                                    ::Pair,
-                                )
-                                .map { (bounds, _) -> bounds }
-                                .collect { wallpaperFocalAreaBounds ->
-                                    wallpaperManager.sendWallpaperCommand(
-                                        /* windowToken = */ rootView?.windowToken,
-                                        /* action = */ WallpaperManager
-                                            .COMMAND_LOCKSCREEN_LAYOUT_CHANGED,
-                                        /* x = */ 0,
-                                        /* y = */ 0,
-                                        /* z = */ 0,
-                                        /* extras = */ Bundle().apply {
-                                            putFloat(
-                                                "wallpaperFocalAreaLeft",
-                                                wallpaperFocalAreaBounds.left,
-                                            )
-                                            putFloat(
-                                                "wallpaperFocalAreaRight",
-                                                wallpaperFocalAreaBounds.right,
-                                            )
-                                            putFloat(
-                                                "wallpaperFocalAreaTop",
-                                                wallpaperFocalAreaBounds.top,
-                                            )
-                                            putFloat(
-                                                "wallpaperFocalAreaBottom",
-                                                wallpaperFocalAreaBounds.bottom,
-                                            )
-                                        },
-                                    )
-                                }
-                        }
-
-                    sendTapInShapeEffectsJob =
-                        scope.launch {
-                            wallpaperFocalAreaRepository.wallpaperFocalAreaTapPosition.collect {
-                                wallpaperFocalAreaTapPosition ->
-                                wallpaperManager.sendWallpaperCommand(
-                                    /* windowToken = */ rootView?.windowToken,
-                                    /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP,
-                                    /* x = */ wallpaperFocalAreaTapPosition.x.toInt(),
-                                    /* y = */ wallpaperFocalAreaTapPosition.y.toInt(),
-                                    /* z = */ 0,
-                                    /* extras = */ null,
-                                )
-                            }
-                        }
-                } else {
-                    sendLockscreenLayoutJob?.cancel()
-                    sendTapInShapeEffectsJob?.cancel()
-                }
                 shouldSendNotificationLayout
             }
             .stateIn(
@@ -227,4 +189,9 @@
             wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id)
         }
     }
+
+    companion object {
+        private val TAG = WallpaperRepositoryImpl::class.simpleName
+        private val DEBUG = true
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
index 187d6c7..09c6cdf 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
@@ -20,60 +20,38 @@
 import android.content.res.Resources
 import android.graphics.PointF
 import android.graphics.RectF
+import android.util.Log
 import android.util.TypedValue
-import androidx.annotation.VisibleForTesting
 import com.android.app.animation.MathUtils
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.wallpapers.data.repository.WallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.WallpaperRepository
 import javax.inject.Inject
 import kotlin.math.min
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 
 @SysUISingleton
 class WallpaperFocalAreaInteractor
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
     private val context: Context,
     private val wallpaperFocalAreaRepository: WallpaperFocalAreaRepository,
     shadeRepository: ShadeRepository,
-    activeNotificationsInteractor: ActiveNotificationsInteractor,
-    val wallpaperRepository: WallpaperRepository,
 ) {
-    // When there's notifications in splitshade, the focal area should be left aligned
-    @VisibleForTesting
-    val notificationInShadeWideLayout: Flow<Boolean> =
-        combine(
-            shadeRepository.isShadeLayoutWide,
-            activeNotificationsInteractor.areAnyNotificationsPresent,
-        ) { isShadeLayoutWide, areAnyNotificationsPresent: Boolean ->
-            when {
-                !isShadeLayoutWide -> false
-                !areAnyNotificationsPresent -> false
-                else -> true
-            }
-        }
-
-    val hasFocalArea = wallpaperRepository.shouldSendFocalArea
+    val hasFocalArea = wallpaperFocalAreaRepository.hasFocalArea
 
     val wallpaperFocalAreaBounds: Flow<RectF> =
         combine(
                 shadeRepository.isShadeLayoutWide,
-                notificationInShadeWideLayout,
                 wallpaperFocalAreaRepository.notificationStackAbsoluteBottom,
                 wallpaperFocalAreaRepository.shortcutAbsoluteTop,
                 wallpaperFocalAreaRepository.notificationDefaultTop,
             ) {
                 isShadeLayoutWide,
-                notificationInShadeWideLayout,
                 notificationStackAbsoluteBottom,
                 shortcutAbsoluteTop,
                 notificationDefaultTop ->
@@ -97,28 +75,21 @@
                         screenBounds.centerY() + screenBounds.height() / 2F / wallpaperZoomedInScale,
                     )
 
+                val focalAreaMaxWidthDp = getFocalAreaMaxWidthDp(context)
                 val maxFocalAreaWidth =
                     TypedValue.applyDimension(
                         TypedValue.COMPLEX_UNIT_DIP,
-                        FOCAL_AREA_MAX_WIDTH_DP.toFloat(),
+                        focalAreaMaxWidthDp.toFloat(),
                         context.resources.displayMetrics,
                     )
 
                 val (left, right) =
-                // tablet landscape
-                if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) {
+                // Tablet & unfold foldable landscape
+                if (isShadeLayoutWide) {
                         Pair(
                             scaledBounds.centerX() - maxFocalAreaWidth / 2F,
                             scaledBounds.centerX() + maxFocalAreaWidth / 2F,
                         )
-                        // unfold foldable landscape
-                    } else if (isShadeLayoutWide) {
-                        if (notificationInShadeWideLayout) {
-                            Pair(scaledBounds.left, scaledBounds.centerX())
-                        } else {
-                            Pair(scaledBounds.centerX(), scaledBounds.right)
-                        }
-                        // handheld / portrait
                     } else {
                         val focalAreaWidth = min(scaledBounds.width(), maxFocalAreaWidth)
                         Pair(
@@ -147,8 +118,10 @@
                                 wallpaperZoomedInScale
                     }
                 val bottom = scaledBounds.bottom - scaledBottomMargin
-                RectF(left, top, right, bottom)
+                RectF(left, top, right, bottom).also { Log.d(TAG, "Focal area changes to $it") }
             }
+            // Make sure a valid rec
+            .filter { it.width() >= 0 && it.height() >= 0 }
             .distinctUntilChanged()
 
     fun setFocalAreaBounds(bounds: RectF) {
@@ -187,8 +160,17 @@
             return if (scale == 0f) 1f else scale
         }
 
-        // A max width for focal area shape effects bounds, to avoid
-        // it becoming too large in large screen portrait mode
-        const val FOCAL_AREA_MAX_WIDTH_DP = 500
+        // A max width for focal area shape effects bounds, to avoid it becoming too large,
+        // especially in portrait mode
+        const val FOCAL_AREA_MAX_WIDTH_DP_TABLET = 500
+        const val FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE = 400
+
+        fun getFocalAreaMaxWidthDp(context: Context): Int {
+            return if (context.resources.getBoolean(R.bool.center_align_focal_area_shape))
+                FOCAL_AREA_MAX_WIDTH_DP_TABLET
+            else FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE
+        }
+
+        private val TAG = WallpaperFocalAreaInteractor::class.simpleName
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
index 70a97d4..4cd49d0 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
@@ -17,15 +17,41 @@
 package com.android.systemui.wallpapers.ui.viewmodel
 
 import android.graphics.RectF
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor
 import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
 
 class WallpaperFocalAreaViewModel
 @Inject
-constructor(private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor) {
+constructor(
+    private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
+    val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) {
     val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea
 
-    val wallpaperFocalAreaBounds = wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds
+    val wallpaperFocalAreaBounds =
+        combine(
+                wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds,
+                keyguardTransitionInteractor
+                    .transition(
+                        edge = Edge.create(to = Scenes.Lockscreen),
+                        edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN),
+                    )
+                    .filter { transitionStep ->
+                        // Should not filter by TransitionState.STARTED, it may race with
+                        // wakingup command, causing layout change command not be received.
+                        transitionStep.transitionState == TransitionState.FINISHED
+                    },
+                ::Pair,
+            )
+            .map { (bounds, _) -> bounds }
 
     fun setFocalAreaBounds(bounds: RectF) {
         wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
index dcf38800..14a81b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
@@ -30,7 +30,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.eq
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
@@ -60,10 +59,11 @@
         val textAnimator =
             TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
                 this.textInterpolator = textInterpolator
+                this.createAnimator = { valueAnimator }
                 this.animator = valueAnimator
             }
 
-        textAnimator.setTextStyle(weight = 400, animate = true)
+        textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), TextAnimator.Animation())
 
         // If animation is requested, the base state should be rebased and the target state should
         // be updated.
@@ -90,10 +90,11 @@
         val textAnimator =
             TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
                 this.textInterpolator = textInterpolator
+                this.createAnimator = { valueAnimator }
                 this.animator = valueAnimator
             }
 
-        textAnimator.setTextStyle(weight = 400, animate = false)
+        textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"))
 
         // If animation is not requested, the progress should be 1 which is end of animation and the
         // base state is rebased to target state by calling rebase.
@@ -118,23 +119,24 @@
         val textAnimator =
             TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
                 this.textInterpolator = textInterpolator
+                this.createAnimator = { valueAnimator }
                 this.animator = valueAnimator
             }
 
         textAnimator.setTextStyle(
-            weight = 400,
-            animate = true,
-            onAnimationEnd = animationEndCallback,
+            TextAnimator.Style("'wght' 400"),
+            TextAnimator.Animation(animate = true, onAnimationEnd = animationEndCallback),
         )
 
         // Verify animationEnd callback has been added.
         val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java)
-        verify(valueAnimator).addListener(captor.capture())
-        captor.value.onAnimationEnd(valueAnimator)
+        verify(valueAnimator, times(2)).addListener(captor.capture())
+        for (callback in captor.allValues) {
+            callback.onAnimationEnd(valueAnimator)
+        }
 
         // Verify animationEnd callback has been invoked and removed.
         verify(animationEndCallback).run()
-        verify(valueAnimator).removeListener(eq(captor.value))
     }
 
     @Test
@@ -148,18 +150,20 @@
         val textAnimator =
             TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
                 this.textInterpolator = textInterpolator
+                this.createAnimator = { valueAnimator }
                 this.animator = valueAnimator
             }
 
-        textAnimator.setTextStyle(weight = 400, animate = true)
+        val animation = TextAnimator.Animation(animate = true)
+        textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation)
 
         val prevTypeface = paint.typeface
 
-        textAnimator.setTextStyle(weight = 700, animate = true)
+        textAnimator.setTextStyle(TextAnimator.Style("'wght' 700"), animation)
 
         assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface)
 
-        textAnimator.setTextStyle(weight = 400, animate = true)
+        textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation)
 
         assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt
index bfc5361..f04fc86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.volume.domain.interactor.audioModeInteractor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -146,6 +147,7 @@
                     )
                 ),
                 kosmos.audioSharingInteractor,
+                kosmos.audioModeInteractor,
                 kosmos.audioSharingButtonViewModelFactory,
                 bluetoothDeviceMetadataInteractor,
                 mDialogTransitionAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 0aa5199..7c8822b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothDevice
 import android.graphics.drawable.Drawable
-import android.media.AudioManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
@@ -61,8 +60,6 @@
     private val connectedDeviceItemFactory = ConnectedDeviceItemFactory()
     private val savedDeviceItemFactory = SavedDeviceItemFactory()
 
-    private val audioManager = context.getSystemService(AudioManager::class.java)!!
-
     @Before
     fun setup() {
         mockitoSession =
@@ -132,7 +129,12 @@
     fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() {
         assertThat(
                 AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)
-                    .isFilterMatched(context, cachedDevice, audioManager, false)
+                    .isFilterMatched(
+                        context,
+                        cachedDevice,
+                        isOngoingCall = false,
+                        audioSharingAvailable = false,
+                    )
             )
             .isFalse()
     }
@@ -143,7 +145,12 @@
 
         assertThat(
                 AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)
-                    .isFilterMatched(context, cachedDevice, audioManager, true)
+                    .isFilterMatched(
+                        context,
+                        cachedDevice,
+                        isOngoingCall = false,
+                        audioSharingAvailable = true,
+                    )
             )
             .isFalse()
     }
@@ -157,7 +164,26 @@
 
         assertThat(
                 AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)
-                    .isFilterMatched(context, cachedDevice, audioManager, true)
+                    .isFilterMatched(
+                        context,
+                        cachedDevice,
+                        isOngoingCall = false,
+                        audioSharingAvailable = true,
+                    )
+            )
+            .isFalse()
+    }
+
+    @Test
+    fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_inCall_false() {
+        assertThat(
+                AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)
+                    .isFilterMatched(
+                        context,
+                        cachedDevice,
+                        isOngoingCall = true,
+                        audioSharingAvailable = true,
+                    )
             )
             .isFalse()
     }
@@ -171,7 +197,12 @@
 
         assertThat(
                 AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)
-                    .isFilterMatched(context, cachedDevice, audioManager, true)
+                    .isFilterMatched(
+                        context,
+                        cachedDevice,
+                        isOngoingCall = false,
+                        audioSharingAvailable = true,
+                    )
             )
             .isTrue()
     }
@@ -182,7 +213,9 @@
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(false)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isTrue()
     }
 
@@ -192,7 +225,9 @@
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(true)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isFalse()
     }
 
@@ -201,7 +236,9 @@
     fun testSavedFactory_isFilterMatched_notBonded_returnsFalse() {
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isFalse()
     }
 
@@ -211,7 +248,9 @@
         `when`(cachedDevice.device).thenReturn(bluetoothDevice)
         `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(true)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isFalse()
     }
 
@@ -223,7 +262,9 @@
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(false)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isTrue()
     }
 
@@ -235,7 +276,9 @@
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(true)
 
-        assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                savedDeviceItemFactory.isFilterMatched(context, cachedDevice, isOngoingCall = false)
+            )
             .isFalse()
     }
 
@@ -244,14 +287,26 @@
     fun testConnectedFactory_isFilterMatched_bondedAndConnected_returnsTrue() {
         `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(true)
 
-        assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                connectedDeviceItemFactory.isFilterMatched(
+                    context,
+                    cachedDevice,
+                    isOngoingCall = false,
+                )
+            )
             .isTrue()
     }
 
     @Test
     @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
     fun testConnectedFactory_isFilterMatched_notConnected_returnsFalse() {
-        assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                connectedDeviceItemFactory.isFilterMatched(
+                    context,
+                    cachedDevice,
+                    isOngoingCall = false,
+                )
+            )
             .isFalse()
     }
 
@@ -261,7 +316,13 @@
         `when`(cachedDevice.device).thenReturn(bluetoothDevice)
         `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(true)
 
-        assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                connectedDeviceItemFactory.isFilterMatched(
+                    context,
+                    cachedDevice,
+                    isOngoingCall = false,
+                )
+            )
             .isFalse()
     }
 
@@ -272,7 +333,13 @@
         `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false)
         `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(true)
 
-        assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                connectedDeviceItemFactory.isFilterMatched(
+                    context,
+                    cachedDevice,
+                    isOngoingCall = false,
+                )
+            )
             .isTrue()
     }
 
@@ -283,7 +350,13 @@
         `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false)
         `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(false)
 
-        assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager))
+        assertThat(
+                connectedDeviceItemFactory.isFilterMatched(
+                    context,
+                    cachedDevice,
+                    isOngoingCall = false,
+                )
+            )
             .isFalse()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index 42dc50d..943b89a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.volume.domain.interactor.audioModeInteractor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -102,7 +103,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -111,6 +111,7 @@
                     emptyList(),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
@@ -130,7 +131,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -139,6 +139,7 @@
                     emptyList(),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
@@ -158,7 +159,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -167,6 +167,7 @@
                     emptyList(),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
@@ -186,7 +187,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -198,6 +198,7 @@
                     emptyList(),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
@@ -217,7 +218,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -238,6 +238,7 @@
                     ),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
             `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
             `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
@@ -259,7 +260,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -277,6 +277,7 @@
                     listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
             `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
             `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
@@ -300,7 +301,6 @@
                 DeviceItemInteractor(
                     bluetoothTileDialogRepository,
                     kosmos.audioSharingInteractor,
-                    audioManager,
                     adapter,
                     localBluetoothManager,
                     fakeSystemClock,
@@ -309,6 +309,7 @@
                     emptyList(),
                     testScope.backgroundScope,
                     dispatcher,
+                    kosmos.audioModeInteractor,
                 )
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -327,7 +328,7 @@
             override fun isFilterMatched(
                 context: Context,
                 cachedDevice: CachedBluetoothDevice,
-                audioManager: AudioManager,
+                isOngoingCall: Boolean,
                 audioSharingAvailable: Boolean,
             ) = isFilterMatchFunc(cachedDevice)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index a2bd5ec..aaf5559 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -66,6 +66,7 @@
 import com.android.systemui.scene.data.repository.setSceneTransition
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -203,6 +204,7 @@
                 mediaCarouselViewModel = kosmos.mediaCarouselViewModel,
                 mediaViewControllerFactory = mediaViewControllerFactory,
                 deviceEntryInteractor = kosmos.deviceEntryInteractor,
+                mediaControlChipInteractor = kosmos.mediaControlChipInteractor,
             )
         verify(configurationController).addCallback(capture(configListener))
         verify(visualStabilityProvider)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 23282b1..205ccea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -84,7 +84,7 @@
     private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this);
 
     // Mock
-    private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
+    private MediaOutputAdapterBase mMediaOutputBaseAdapter = mock(MediaOutputAdapterBase.class);
     private MediaController mMediaController = mock(MediaController.class);
     private PlaybackState mPlaybackState = mock(PlaybackState.class);
     private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
@@ -219,7 +219,6 @@
     public void refresh_withIconCompat_iconIsVisible() {
         mIconCompat = IconCompat.createWithBitmap(
                 Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
-        when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController);
 
         mMediaOutputBaseDialogImpl.refresh();
         final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 944604f..ae8b52a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -18,7 +18,7 @@
 
 import android.graphics.Insets
 import android.graphics.Rect
-import android.os.PowerManager
+import android.os.powerManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.testing.AndroidTestingRunner
@@ -32,7 +32,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
-import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
 import com.android.systemui.Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX
 import com.android.systemui.SysuiTestCase
@@ -40,7 +40,6 @@
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
@@ -58,8 +57,10 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.controller.keyguardMediaController
 import com.android.systemui.res.R
@@ -69,8 +70,8 @@
 import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Assert.assertThrows
@@ -89,33 +90,23 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
+@EnableFlags(FLAG_COMMUNAL_HUB)
 class GlanceableHubContainerControllerTest : SysuiTestCase() {
-    private val kosmos: Kosmos =
-        testKosmos().apply {
-            // UnconfinedTestDispatcher makes testing simpler due to CommunalInteractor flows using
-            // SharedFlow
-            testDispatcher = UnconfinedTestDispatcher()
-        }
+    private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher()
 
-    private var communalViewModel = mock<CommunalViewModel>()
-    private var powerManager = mock<PowerManager>()
-    private var touchMonitor = mock<TouchMonitor>()
-    private var communalColors = mock<CommunalColors>()
-    private var communalContent = mock<CommunalContent>()
-    private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
+    private val swipeToHubEnabled = MutableStateFlow(false)
+    private val mockCommunalViewModel =
+        mock<CommunalViewModel> { on { swipeToHubEnabled } doReturn swipeToHubEnabled }
+    private val touchMonitor = mock<TouchMonitor>()
 
     private lateinit var parentView: FrameLayout
     private lateinit var containerView: View
-    private lateinit var testableLooper: TestableLooper
 
-    private lateinit var communalRepository: FakeCommunalSceneRepository
-    private lateinit var underTest: GlanceableHubContainerController
+    private val Kosmos.testableLooper by
+        Kosmos.Fixture { TestableLooper.get(this@GlanceableHubContainerControllerTest) }
 
-    @Before
-    fun setUp() {
-        communalRepository = kosmos.fakeCommunalSceneRepository
-
-        ambientTouchComponentFactory =
+    private val Kosmos.ambientTouchComponentFactory by
+        Kosmos.Fixture {
             object : AmbientTouchComponent.Factory {
                 override fun create(
                     lifecycleOwner: LifecycleOwner,
@@ -126,50 +117,39 @@
                         override fun getTouchMonitor(): TouchMonitor = touchMonitor
                     }
             }
-
-        with(kosmos) {
-            underTest =
-                GlanceableHubContainerController(
-                    communalInteractor,
-                    communalSettingsInteractor,
-                    communalViewModel,
-                    keyguardInteractor,
-                    keyguardTransitionInteractor,
-                    shadeInteractor,
-                    powerManager,
-                    communalColors,
-                    ambientTouchComponentFactory,
-                    communalContent,
-                    sceneDataSourceDelegator,
-                    notificationStackScrollLayoutController,
-                    keyguardMediaController,
-                    lockscreenSmartspaceController,
-                    logcatLogBuffer("GlanceableHubContainerControllerTest"),
-                )
-
-            // Make below last notification true by default or else touches will be ignored by
-            // default when the hub is not showing.
-            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
-                .thenReturn(true)
         }
-        testableLooper = TestableLooper.get(this)
 
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            GlanceableHubContainerController(
+                communalInteractor,
+                communalSettingsInteractor,
+                mockCommunalViewModel,
+                keyguardInteractor,
+                keyguardTransitionInteractor,
+                shadeInteractor,
+                powerManager,
+                mock<CommunalColors>(),
+                ambientTouchComponentFactory,
+                mock<CommunalContent>(),
+                sceneDataSourceDelegator,
+                notificationStackScrollLayoutController,
+                keyguardMediaController,
+                lockscreenSmartspaceController,
+                logcatLogBuffer("GlanceableHubContainerControllerTest"),
+            )
+        }
+
+    @Before
+    fun setUp() {
         overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
         overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
         overrideResource(
             R.dimen.communal_bottom_edge_swipe_region_height,
             BOTTOM_SWIPE_REGION_WIDTH,
         )
-
-        // Make communal available so that communalInteractor.desiredScene accurately reflects
-        // scene changes instead of just returning Blank.
-        mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
-        with(kosmos.testScope) {
-            launch { kosmos.setCommunalAvailable(true) }
-            testScheduler.runCurrent()
-        }
-
-        initAndAttachContainerView()
+        runBlocking { kosmos.setCommunalAvailable(true) }
+        kosmos.initAndAttachContainerView()
     }
 
     @After
@@ -179,606 +159,531 @@
 
     @Test
     fun initView_calledTwice_throwsException() =
-        with(kosmos) {
-            testScope.runTest {
-                underTest =
-                    GlanceableHubContainerController(
-                        communalInteractor,
-                        kosmos.communalSettingsInteractor,
-                        communalViewModel,
-                        keyguardInteractor,
-                        kosmos.keyguardTransitionInteractor,
-                        shadeInteractor,
-                        powerManager,
-                        communalColors,
-                        ambientTouchComponentFactory,
-                        communalContent,
-                        kosmos.sceneDataSourceDelegator,
-                        kosmos.notificationStackScrollLayoutController,
-                        kosmos.keyguardMediaController,
-                        kosmos.lockscreenSmartspaceController,
-                        logcatLogBuffer("GlanceableHubContainerControllerTest"),
-                    )
+        kosmos.runTest {
+            val controller =
+                GlanceableHubContainerController(
+                    communalInteractor,
+                    communalSettingsInteractor,
+                    mockCommunalViewModel,
+                    keyguardInteractor,
+                    keyguardTransitionInteractor,
+                    shadeInteractor,
+                    powerManager,
+                    mock<CommunalColors>(),
+                    ambientTouchComponentFactory,
+                    mock<CommunalContent>(),
+                    sceneDataSourceDelegator,
+                    notificationStackScrollLayoutController,
+                    keyguardMediaController,
+                    lockscreenSmartspaceController,
+                    logcatLogBuffer("GlanceableHubContainerControllerTest"),
+                )
 
-                // First call succeeds.
-                underTest.initView(context)
+            // First call succeeds.
+            controller.initView(context)
 
-                // Second call throws.
-                assertThrows(RuntimeException::class.java) { underTest.initView(context) }
-            }
+            // Second call throws.
+            assertThrows(RuntimeException::class.java) { controller.initView(context) }
         }
 
     @Test
     fun lifecycle_initializedAfterConstruction() =
-        with(kosmos) {
-            val underTest =
+        kosmos.runTest {
+            val controller =
                 GlanceableHubContainerController(
                     communalInteractor,
-                    kosmos.communalSettingsInteractor,
-                    communalViewModel,
+                    communalSettingsInteractor,
+                    mockCommunalViewModel,
                     keyguardInteractor,
-                    kosmos.keyguardTransitionInteractor,
+                    keyguardTransitionInteractor,
                     shadeInteractor,
                     powerManager,
-                    communalColors,
+                    mock<CommunalColors>(),
                     ambientTouchComponentFactory,
-                    communalContent,
-                    kosmos.sceneDataSourceDelegator,
-                    kosmos.notificationStackScrollLayoutController,
-                    kosmos.keyguardMediaController,
-                    kosmos.lockscreenSmartspaceController,
+                    mock<CommunalContent>(),
+                    sceneDataSourceDelegator,
+                    notificationStackScrollLayoutController,
+                    keyguardMediaController,
+                    lockscreenSmartspaceController,
                     logcatLogBuffer("GlanceableHubContainerControllerTest"),
                 )
 
-            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+            assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
         }
 
     @Test
     fun lifecycle_createdAfterViewCreated() =
-        with(kosmos) {
-            val underTest =
+        kosmos.runTest {
+            val controller =
                 GlanceableHubContainerController(
                     communalInteractor,
-                    kosmos.communalSettingsInteractor,
-                    communalViewModel,
+                    communalSettingsInteractor,
+                    mockCommunalViewModel,
                     keyguardInteractor,
-                    kosmos.keyguardTransitionInteractor,
+                    keyguardTransitionInteractor,
                     shadeInteractor,
                     powerManager,
-                    communalColors,
+                    mock<CommunalColors>(),
                     ambientTouchComponentFactory,
-                    communalContent,
-                    kosmos.sceneDataSourceDelegator,
-                    kosmos.notificationStackScrollLayoutController,
-                    kosmos.keyguardMediaController,
-                    kosmos.lockscreenSmartspaceController,
+                    mock<CommunalContent>(),
+                    sceneDataSourceDelegator,
+                    notificationStackScrollLayoutController,
+                    keyguardMediaController,
+                    lockscreenSmartspaceController,
                     logcatLogBuffer("GlanceableHubContainerControllerTest"),
                 )
 
             // Only initView without attaching a view as we don't want the flows to start collecting
             // yet.
-            underTest.initView(View(context))
+            controller.initView(View(context))
+
+            assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        }
+
+    @Test
+    fun lifecycle_startedAfterFlowsUpdate() =
+        kosmos.runTest {
+            // Flows start collecting due to test setup, causing the state to advance to STARTED.
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_resumedAfterCommunalShows() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        }
+
+    @Test
+    fun lifecycle_startedAfterCommunalCloses() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+            // Communal closes.
+            goToScene(CommunalScenes.Blank)
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_startedAfterPrimaryBouncerShows() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            // Bouncer is visible.
+            fakeKeyguardBouncerRepository.setPrimaryShow(true)
+            testableLooper.processAllMessages()
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_startedAfterAlternateBouncerShows() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            // Bouncer is visible.
+            fakeKeyguardBouncerRepository.setAlternateVisible(true)
+            testableLooper.processAllMessages()
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_startedWhenEditActivityShowing() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            // Edit activity is showing.
+            communalInteractor.setEditActivityShowing(true)
+            testableLooper.processAllMessages()
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_startedWhenEditModeTransitionStarted() =
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
+
+            // Leaving edit mode to return to the hub.
+            fakeKeyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    value = 1.0f,
+                    transitionState = TransitionState.RUNNING,
+                )
+            )
+            testableLooper.processAllMessages()
+
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        }
+
+    @Test
+    fun lifecycle_createdAfterDisposeView() =
+        kosmos.runTest {
+            // Container view disposed.
+            underTest.disposeView()
 
             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
         }
 
     @Test
-    fun lifecycle_startedAfterFlowsUpdate() {
-        // Flows start collecting due to test setup, causing the state to advance to STARTED.
-        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-    }
-
-    @Test
-    fun lifecycle_resumedAfterCommunalShows() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
-            }
-        }
-
-    @Test
-    fun lifecycle_startedAfterCommunalCloses() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
-
-                // Communal closes.
-                goToScene(CommunalScenes.Blank)
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
-        }
-
-    @Test
-    fun lifecycle_startedAfterPrimaryBouncerShows() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Bouncer is visible.
-                fakeKeyguardBouncerRepository.setPrimaryShow(true)
-                testableLooper.processAllMessages()
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
-        }
-
-    @Test
-    fun lifecycle_startedAfterAlternateBouncerShows() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Bouncer is visible.
-                fakeKeyguardBouncerRepository.setAlternateVisible(true)
-                testableLooper.processAllMessages()
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
-        }
-
-    @Test
-    fun lifecycle_startedWhenEditActivityShowing() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Edit activity is showing.
-                communalInteractor.setEditActivityShowing(true)
-                testableLooper.processAllMessages()
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
-        }
-
-    @Test
-    fun lifecycle_startedWhenEditModeTransitionStarted() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
-
-                // Leaving edit mode to return to the hub.
-                fakeKeyguardTransitionRepository.sendTransitionStep(
-                    TransitionStep(
-                        from = KeyguardState.GONE,
-                        to = KeyguardState.GLANCEABLE_HUB,
-                        value = 1.0f,
-                        transitionState = TransitionState.RUNNING,
-                    )
-                )
-                testableLooper.processAllMessages()
-
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
-        }
-
-    @Test
-    fun lifecycle_createdAfterDisposeView() {
-        // Container view disposed.
-        underTest.disposeView()
-
-        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
-    }
-
-    @Test
     fun lifecycle_startedAfterShadeShows() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Shade shows up.
-                shadeTestUtil.setQsExpansion(1.0f)
-                testableLooper.processAllMessages()
+            // Shade shows up.
+            shadeTestUtil.setQsExpansion(1.0f)
+            testableLooper.processAllMessages()
 
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
-            }
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
         }
 
     @Test
     fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Shade shows up.
-                shadeTestUtil.setShadeExpansion(1.0f)
-                testableLooper.processAllMessages()
-                underTest.onTouchEvent(DOWN_EVENT)
-                testableLooper.processAllMessages()
+            // Shade shows up.
+            shadeTestUtil.setShadeExpansion(1.0f)
+            testableLooper.processAllMessages()
+            underTest.onTouchEvent(DOWN_EVENT)
+            testableLooper.processAllMessages()
 
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
 
-                // Shade starts collapsing.
-                shadeTestUtil.setShadeExpansion(.5f)
-                testableLooper.processAllMessages()
-                underTest.onTouchEvent(DOWN_EVENT)
-                testableLooper.processAllMessages()
+            // Shade starts collapsing.
+            shadeTestUtil.setShadeExpansion(.5f)
+            testableLooper.processAllMessages()
+            underTest.onTouchEvent(DOWN_EVENT)
+            testableLooper.processAllMessages()
 
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
 
-                // Shade fully collpase, and then expand should with touch interaction should now
-                // be resumed.
-                shadeTestUtil.setShadeExpansion(0f)
-                testableLooper.processAllMessages()
-                shadeTestUtil.setShadeExpansion(.5f)
-                testableLooper.processAllMessages()
-                underTest.onTouchEvent(DOWN_EVENT)
-                testableLooper.processAllMessages()
+            // Shade fully collpase, and then expand should with touch interaction should now
+            // be resumed.
+            shadeTestUtil.setShadeExpansion(0f)
+            testableLooper.processAllMessages()
+            shadeTestUtil.setShadeExpansion(.5f)
+            testableLooper.processAllMessages()
+            underTest.onTouchEvent(DOWN_EVENT)
+            testableLooper.processAllMessages()
 
-                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
-            }
+            assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
         }
 
     @Test
     fun touchHandling_moveEventProcessedAfterCancel() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Touch starts and ends.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
-                assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue()
+            // Touch starts and ends.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+            assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue()
 
-                // Up event is no longer processed
-                assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
+            // Up event is no longer processed
+            assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
 
-                // Move event can still be processed
-                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
-                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
-                assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
-            }
+            // Move event can still be processed
+            assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
+            assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
+            assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
         }
 
     @Test
     fun editMode_communalAvailable() =
-        with(kosmos) {
-            testScope.runTest {
-                val available by collectLastValue(underTest.communalAvailable())
-                setCommunalAvailable(false)
+        kosmos.runTest {
+            val available by collectLastValue(underTest.communalAvailable())
+            setCommunalAvailable(false)
 
-                assertThat(available).isFalse()
-                communalInteractor.setEditModeOpen(true)
-                assertThat(available).isTrue()
-            }
+            assertThat(available).isFalse()
+            communalInteractor.setEditModeOpen(true)
+            assertThat(available).isTrue()
         }
 
     @Test
     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun gestureExclusionZone_setAfterInit() =
-        with(kosmos) {
-            testScope.runTest {
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            goToScene(CommunalScenes.Communal)
 
-                assertThat(containerView.systemGestureExclusionRects)
-                    .containsExactly(
-                        Rect(
-                            /* left= */ 0,
-                            /* top= */ TOP_SWIPE_REGION_WIDTH,
-                            /* right= */ CONTAINER_WIDTH,
-                            /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH,
-                        )
+            assertThat(containerView.systemGestureExclusionRects)
+                .containsExactly(
+                    Rect(
+                        /* left= */ 0,
+                        /* top= */ TOP_SWIPE_REGION_WIDTH,
+                        /* right= */ CONTAINER_WIDTH,
+                        /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH,
                     )
-            }
+                )
         }
 
     @Test
     @EnableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun gestureExclusionZone_setAfterInit_fullSwipe() =
-        with(kosmos) {
-            testScope.runTest {
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            goToScene(CommunalScenes.Communal)
 
-                assertThat(containerView.systemGestureExclusionRects).isEmpty()
-            }
+            assertThat(containerView.systemGestureExclusionRects).isEmpty()
         }
 
     @Test
     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun gestureExclusionZone_unsetWhenShadeOpen() =
-        with(kosmos) {
-            testScope.runTest {
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            goToScene(CommunalScenes.Communal)
 
-                // Exclusion rect is set.
-                assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
+            // Exclusion rect is set.
+            assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
 
-                // Shade shows up.
-                shadeTestUtil.setQsExpansion(1.0f)
-                testableLooper.processAllMessages()
+            // Shade shows up.
+            shadeTestUtil.setQsExpansion(1.0f)
+            testableLooper.processAllMessages()
 
-                // Exclusion rects are unset.
-                assertThat(containerView.systemGestureExclusionRects).isEmpty()
-            }
+            // Exclusion rects are unset.
+            assertThat(containerView.systemGestureExclusionRects).isEmpty()
         }
 
     @Test
     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun gestureExclusionZone_unsetWhenBouncerOpen() =
-        with(kosmos) {
-            testScope.runTest {
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            goToScene(CommunalScenes.Communal)
 
-                // Exclusion rect is set.
-                assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
+            // Exclusion rect is set.
+            assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
 
-                // Bouncer is visible.
-                fakeKeyguardBouncerRepository.setPrimaryShow(true)
-                testableLooper.processAllMessages()
+            // Bouncer is visible.
+            fakeKeyguardBouncerRepository.setPrimaryShow(true)
+            testableLooper.processAllMessages()
 
-                // Exclusion rects are unset.
-                assertThat(containerView.systemGestureExclusionRects).isEmpty()
-            }
+            // Exclusion rects are unset.
+            assertThat(containerView.systemGestureExclusionRects).isEmpty()
         }
 
     @Test
     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun gestureExclusionZone_unsetWhenHubClosed() =
-        with(kosmos) {
-            testScope.runTest {
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            goToScene(CommunalScenes.Communal)
 
-                // Exclusion rect is set.
-                assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
+            // Exclusion rect is set.
+            assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
 
-                // Leave the hub.
-                goToScene(CommunalScenes.Blank)
+            // Leave the hub.
+            goToScene(CommunalScenes.Blank)
 
-                // Exclusion rect is unset.
-                assertThat(containerView.systemGestureExclusionRects).isEmpty()
-            }
+            // Exclusion rect is unset.
+            assertThat(containerView.systemGestureExclusionRects).isEmpty()
         }
 
     @Test
     fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is closed.
-                goToScene(CommunalScenes.Blank)
-                whenever(
-                        notificationStackScrollLayoutController.isBelowLastNotification(
-                            any(),
-                            any(),
-                        )
-                    )
-                    .thenReturn(false)
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-            }
+        kosmos.runTest {
+            // Communal is closed.
+            goToScene(CommunalScenes.Blank)
+            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
+                .thenReturn(false)
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
         }
 
     @Test
     fun fullScreenSwipeGesture_doNotProcessTouchesInUmo() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is closed.
-                goToScene(CommunalScenes.Blank)
-                whenever(keyguardMediaController.isWithinMediaViewBounds(any(), any()))
-                    .thenReturn(true)
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-            }
+        kosmos.runTest {
+            // Communal is closed.
+            goToScene(CommunalScenes.Blank)
+            whenever(keyguardMediaController.isWithinMediaViewBounds(any(), any())).thenReturn(true)
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
         }
 
     @Test
     fun fullScreenSwipeGesture_doNotProcessTouchesInSmartspace() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is closed.
-                goToScene(CommunalScenes.Blank)
-                whenever(lockscreenSmartspaceController.isWithinSmartspaceBounds(any(), any()))
-                    .thenReturn(true)
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-            }
+        kosmos.runTest {
+            // Communal is closed.
+            goToScene(CommunalScenes.Blank)
+            whenever(lockscreenSmartspaceController.isWithinSmartspaceBounds(any(), any()))
+                .thenReturn(true)
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
         }
 
     @Test
     fun onTouchEvent_hubOpen_touchesDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Touch event is sent to the container view.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
-                verify(containerView).onTouchEvent(DOWN_EVENT)
-                assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
-                verify(containerView).onTouchEvent(UP_EVENT)
-            }
+            // Touch event is sent to the container view.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+            verify(containerView).onTouchEvent(DOWN_EVENT)
+            assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
+            verify(containerView).onTouchEvent(UP_EVENT)
         }
 
     @Test
     fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Transitioning to or from edit mode.
-                communalInteractor.setEditActivityShowing(true)
-                testableLooper.processAllMessages()
+            // Transitioning to or from edit mode.
+            communalInteractor.setEditActivityShowing(true)
+            testableLooper.processAllMessages()
 
-                // onTouchEvent returns true to consume the touch, but it is not sent to the
-                // container view.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
-                verify(containerView, never()).onTouchEvent(any())
-            }
+            // onTouchEvent returns true to consume the touch, but it is not sent to the
+            // container view.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+            verify(containerView, never()).onTouchEvent(any())
         }
 
     @Test
     fun onTouchEvent_editModeTransitionStarted_touchesConsumedButNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                // Communal is open.
-                goToScene(CommunalScenes.Communal)
+        kosmos.runTest {
+            // Communal is open.
+            goToScene(CommunalScenes.Communal)
 
-                // Leaving edit mode to return to the hub.
-                fakeKeyguardTransitionRepository.sendTransitionStep(
-                    TransitionStep(
-                        from = KeyguardState.GONE,
-                        to = KeyguardState.GLANCEABLE_HUB,
-                        value = 1.0f,
-                        transitionState = TransitionState.RUNNING,
-                    )
+            // Leaving edit mode to return to the hub.
+            fakeKeyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    value = 1.0f,
+                    transitionState = TransitionState.RUNNING,
                 )
-                testableLooper.processAllMessages()
+            )
+            testableLooper.processAllMessages()
 
-                // onTouchEvent returns true to consume the touch, but it is not sent to the
-                // container view.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
-                verify(containerView, never()).onTouchEvent(any())
-            }
+            // onTouchEvent returns true to consume the touch, but it is not sent to the
+            // container view.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+            verify(containerView, never()).onTouchEvent(any())
         }
 
     @Test
     fun onTouchEvent_shadeInteracting_movesNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true)
-                // On lockscreen.
-                goToScene(CommunalScenes.Blank)
-                whenever(
-                        notificationStackScrollLayoutController.isBelowLastNotification(
-                            any(),
-                            any(),
-                        )
-                    )
-                    .thenReturn(true)
+        kosmos.runTest {
+            swipeToHubEnabled.value = true
 
-                // Touches not consumed by default but are received by containerView.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-                verify(containerView).onTouchEvent(DOWN_EVENT)
+            // On lockscreen.
+            goToScene(CommunalScenes.Blank)
+            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
+                .thenReturn(true)
 
-                // User is interacting with shade on lockscreen.
-                shadeTestUtil.setLockscreenShadeTracking(true)
-                testableLooper.processAllMessages()
+            // Touches not consumed by default but are received by containerView.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            verify(containerView).onTouchEvent(DOWN_EVENT)
 
-                // A move event is ignored while the user is already interacting.
-                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
-                verify(containerView, never()).onTouchEvent(MOVE_EVENT)
+            // User is interacting with shade on lockscreen.
+            shadeTestUtil.setLockscreenShadeTracking(true)
+            testableLooper.processAllMessages()
 
-                // An up event is still delivered.
-                assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
-                verify(containerView).onTouchEvent(UP_EVENT)
-            }
+            // A move event is ignored while the user is already interacting.
+            assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
+            verify(containerView, never()).onTouchEvent(MOVE_EVENT)
+
+            // An up event is still delivered.
+            assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
+            verify(containerView).onTouchEvent(UP_EVENT)
         }
 
     @Test
     fun onTouchEvent_shadeExpanding_touchesNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                // On lockscreen.
-                goToScene(CommunalScenes.Blank)
-                whenever(
-                        notificationStackScrollLayoutController.isBelowLastNotification(
-                            any(),
-                            any(),
-                        )
-                    )
-                    .thenReturn(true)
+        kosmos.runTest {
+            // On lockscreen.
+            goToScene(CommunalScenes.Blank)
+            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
+                .thenReturn(true)
 
-                // Shade is open slightly.
-                shadeTestUtil.setShadeExpansion(0.01f)
-                testableLooper.processAllMessages()
+            // Shade is open slightly.
+            shadeTestUtil.setShadeExpansion(0.01f)
+            testableLooper.processAllMessages()
 
-                // Touches are not consumed.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-                verify(containerView, never()).onTouchEvent(DOWN_EVENT)
-            }
+            // Touches are not consumed.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            verify(containerView, never()).onTouchEvent(DOWN_EVENT)
         }
 
     @Test
     fun onTouchEvent_qsExpanding_touchesNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                // On lockscreen.
-                goToScene(CommunalScenes.Blank)
-                whenever(
-                        notificationStackScrollLayoutController.isBelowLastNotification(
-                            any(),
-                            any(),
-                        )
-                    )
-                    .thenReturn(true)
+        kosmos.runTest {
+            // On lockscreen.
+            goToScene(CommunalScenes.Blank)
+            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
+                .thenReturn(true)
 
-                // Shade is open slightly.
-                shadeTestUtil.setQsExpansion(0.01f)
-                testableLooper.processAllMessages()
+            // Shade is open slightly.
+            shadeTestUtil.setQsExpansion(0.01f)
+            testableLooper.processAllMessages()
 
-                // Touches are not consumed.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-                verify(containerView, never()).onTouchEvent(DOWN_EVENT)
-            }
+            // Touches are not consumed.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            verify(containerView, never()).onTouchEvent(DOWN_EVENT)
         }
 
     @Test
     fun onTouchEvent_bouncerInteracting_movesNotDispatched() =
-        with(kosmos) {
-            testScope.runTest {
-                `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true)
-                // On lockscreen.
-                goToScene(CommunalScenes.Blank)
-                whenever(
-                        notificationStackScrollLayoutController.isBelowLastNotification(
-                            any(),
-                            any(),
-                        )
-                    )
-                    .thenReturn(true)
+        kosmos.runTest {
+            swipeToHubEnabled.value = true
+            // On lockscreen.
+            goToScene(CommunalScenes.Blank)
+            whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
+                .thenReturn(true)
 
-                // Touches not consumed by default but are received by containerView.
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-                verify(containerView).onTouchEvent(DOWN_EVENT)
+            // Touches not consumed by default but are received by containerView.
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            verify(containerView).onTouchEvent(DOWN_EVENT)
 
-                // User is interacting with bouncer on lockscreen.
-                fakeKeyguardBouncerRepository.setPrimaryShow(true)
-                testableLooper.processAllMessages()
+            // User is interacting with bouncer on lockscreen.
+            fakeKeyguardBouncerRepository.setPrimaryShow(true)
+            testableLooper.processAllMessages()
 
-                // A move event is ignored while the user is already interacting.
-                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
-                verify(containerView, never()).onTouchEvent(MOVE_EVENT)
+            // A move event is ignored while the user is already interacting.
+            assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
+            verify(containerView, never()).onTouchEvent(MOVE_EVENT)
 
-                // An up event is still delivered.
-                assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
-                verify(containerView).onTouchEvent(UP_EVENT)
-            }
+            // An up event is still delivered.
+            assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
+            verify(containerView).onTouchEvent(UP_EVENT)
         }
 
     @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
     @Test
     fun onTouchEvent_onLockscreenAndGlanceableHubV2_touchIgnored() =
-        with(kosmos) {
-            testScope.runTest {
-                kosmos.setCommunalV2ConfigEnabled(true)
+        kosmos.runTest {
+            swipeToHubEnabled.value = false
+            setCommunalV2ConfigEnabled(true)
 
-                // On lockscreen.
-                goToScene(CommunalScenes.Blank)
+            // On lockscreen.
+            goToScene(CommunalScenes.Blank)
 
-                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
-                verify(containerView, never()).onTouchEvent(DOWN_EVENT)
-            }
+            assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            verify(containerView, never()).onTouchEvent(DOWN_EVENT)
         }
 
     @Test
-    fun disposeView_destroysTouchMonitor() {
-        clearInvocations(touchMonitor)
+    fun disposeView_destroysTouchMonitor() =
+        kosmos.runTest {
+            clearInvocations(touchMonitor)
 
-        underTest.disposeView()
+            underTest.disposeView()
 
-        verify(touchMonitor).destroy()
-    }
+            verify(touchMonitor).destroy()
+        }
 
-    private fun initAndAttachContainerView() {
+    private fun Kosmos.initAndAttachContainerView() {
         val mockInsets =
             mock<WindowInsets> {
                 on { getInsets(WindowInsets.Type.systemGestures()) } doReturn FAKE_INSETS
@@ -802,8 +707,8 @@
         testableLooper.processAllMessages()
     }
 
-    private suspend fun goToScene(scene: SceneKey) {
-        communalRepository.changeScene(scene)
+    private suspend fun Kosmos.goToScene(scene: SceneKey) {
+        fakeCommunalSceneRepository.changeScene(scene)
         if (scene == CommunalScenes.Communal) {
             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
similarity index 68%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index e076418..79e78c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -20,9 +20,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.app.animation.Interpolators
-import com.android.systemui.customization.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.FontVariationUtils
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.customization.R
 import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Rule
@@ -32,7 +33,9 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.eq
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -46,6 +49,7 @@
     @Before
     fun setUp() {
         val layoutInflater = LayoutInflater.from(context)
+        whenever(mockTextAnimator.fontVariationUtils).thenReturn(FontVariationUtils())
         clockView =
             layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView
         clockView.textAnimatorFactory = { _, _ -> mockTextAnimator }
@@ -57,18 +61,19 @@
         clockView.animateAppearOnLockscreen()
         clockView.measure(50, 50)
 
+        verify(mockTextAnimator).fontVariationUtils
         verify(mockTextAnimator).glyphFilter = any()
         verify(mockTextAnimator)
             .setTextStyle(
-                weight = 300,
-                textSize = -1.0f,
-                color = 200,
-                strokeWidth = -1F,
-                animate = false,
-                duration = 833L,
-                interpolator = Interpolators.EMPHASIZED_DECELERATE,
-                delay = 0L,
-                onAnimationEnd = null
+                eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
+                eq(
+                    TextAnimator.Animation(
+                        animate = false,
+                        duration = 833L,
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE,
+                        onAnimationEnd = null,
+                    )
+                ),
             )
         verifyNoMoreInteractions(mockTextAnimator)
     }
@@ -79,30 +84,24 @@
         clockView.measure(50, 50)
         clockView.animateAppearOnLockscreen()
 
+        verify(mockTextAnimator, times(2)).fontVariationUtils
         verify(mockTextAnimator, times(2)).glyphFilter = any()
         verify(mockTextAnimator)
             .setTextStyle(
-                weight = 100,
-                textSize = -1.0f,
-                color = 200,
-                strokeWidth = -1F,
-                animate = false,
-                duration = 0L,
-                interpolator = null,
-                delay = 0L,
-                onAnimationEnd = null
+                eq(TextAnimator.Style(fVar = "'wght' 100", color = 200)),
+                eq(TextAnimator.Animation(animate = false, duration = 0)),
             )
+
         verify(mockTextAnimator)
             .setTextStyle(
-                weight = 300,
-                textSize = -1.0f,
-                color = 200,
-                strokeWidth = -1F,
-                animate = true,
-                duration = 833L,
-                interpolator = Interpolators.EMPHASIZED_DECELERATE,
-                delay = 0L,
-                onAnimationEnd = null
+                eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
+                eq(
+                    TextAnimator.Animation(
+                        animate = true,
+                        duration = 833L,
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE,
+                    )
+                ),
             )
         verifyNoMoreInteractions(mockTextAnimator)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
new file mode 100644
index 0000000..510d6fe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.featurepods.popups.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
+import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
+import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnableFlags(StatusBarPopupChips.FLAG_NAME)
+@RunWith(AndroidJUnit4::class)
+class StatusBarPopupChipsViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val underTest = kosmos.statusBarPopupChipsViewModelFactory.create()
+
+    @Before
+    fun setUp() {
+        underTest.activateIn(kosmos.testScope)
+    }
+
+    @Test
+    fun shownPopupChips_allHidden_empty() =
+        kosmos.runTest {
+            val shownPopupChips = underTest.shownPopupChips
+            assertThat(shownPopupChips).isEmpty()
+        }
+
+    @Test
+    fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() =
+        kosmos.runTest {
+            val shownPopupChips = underTest.shownPopupChips
+            val userMedia = MediaData(active = true, song = "test")
+            val instanceId = userMedia.instanceId
+
+            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+
+            Snapshot.takeSnapshot {
+                assertThat(shownPopupChips).hasSize(1)
+                assertThat(shownPopupChips.first().chipId).isEqualTo(PopupChipId.MediaControl)
+            }
+        }
+
+    @Test
+    fun shownPopupChips_mediaChipToggled_popupShown() =
+        kosmos.runTest {
+            val shownPopupChips = underTest.shownPopupChips
+
+            val userMedia = MediaData(active = true, song = "test")
+            val instanceId = userMedia.instanceId
+
+            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+
+            Snapshot.takeSnapshot {
+                assertThat(shownPopupChips).hasSize(1)
+                val mediaChip = shownPopupChips.first()
+                assertThat(mediaChip.isPopupShown).isFalse()
+
+                mediaChip.showPopup.invoke()
+                assertThat(shownPopupChips.first().isPopupShown).isTrue()
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 281ce16..19d1224 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -28,6 +28,8 @@
 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
 import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -46,6 +48,7 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
@@ -59,9 +62,12 @@
 import com.android.systemui.statusbar.SbnBuilder;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -83,6 +89,9 @@
     private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class);
     private final FakeSystemClock mClock = new FakeSystemClock();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setup() {
         Notification.Builder n = new Notification.Builder(mContext, "")
@@ -444,6 +453,145 @@
         // no crash, good
     }
 
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void getParent_adapter() {
+        GroupEntry ge = new GroupEntryBuilder()
+                .build();
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .build();
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .setParent(ge)
+                .build();
+
+        assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent());
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void isTopLevelEntry_adapter() {
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .build();
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .setParent(GroupEntry.ROOT_ENTRY)
+                .build();
+
+        assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void getKey_adapter() {
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .build();
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .build();
+
+        assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey());
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void getRow_adapter() {
+        ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .build();
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .build();
+        entry.setRow(row);
+
+        assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow());
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void getGroupRoot_adapter_groupSummary() {
+        ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setGroupSummary(true)
+                .setGroup("key")
+                .build();
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .setParent(GroupEntry.ROOT_ENTRY)
+                .build();
+        entry.setRow(row);
+
+        assertThat(entry.getEntryAdapter().getGroupRoot()).isNull();
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    public void getGroupRoot_adapter_groupChild() {
+        Notification notification = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setGroupSummary(true)
+                .setGroup("key")
+                .build();
+
+        NotificationEntry parent = new NotificationEntryBuilder()
+                .setParent(GroupEntry.ROOT_ENTRY)
+                .build();
+        GroupEntryBuilder groupEntry = new GroupEntryBuilder()
+                .setSummary(parent);
+
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setChannel(mChannel)
+                .setId(mId++)
+                .setNotification(notification)
+                .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+                .setParent(groupEntry.build())
+                .build();
+
+        assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter());
+    }
 
     private Notification.Action createContextualAction(String title) {
         return new Notification.Action.Builder(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 77b116e..a6722c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES;
+
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
 import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG;
 import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE;
@@ -29,6 +31,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -189,6 +192,54 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
+    public void setSensitive_doesNothingIfCalledAgain() throws Exception {
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        measureAndLayout(row);
+
+        // GIVEN a mocked public layout
+        NotificationContentView mockPublicLayout = mock(NotificationContentView.class);
+        row.setPublicLayout(mockPublicLayout);
+
+        // GIVEN a sensitive notification row that's currently redacted
+        row.setHideSensitiveForIntrinsicHeight(true);
+        row.setSensitive(true, true);
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+        verify(mockPublicLayout).requestSelectLayout(eq(true));
+        clearInvocations(mockPublicLayout);
+
+        // WHEN the row is set to the same sensitive settings
+        row.setSensitive(true, true);
+
+        // VERIFY that the layout is not updated again
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+        verify(mockPublicLayout, never()).requestSelectLayout(anyBoolean());
+    }
+
+    @Test
+    @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
+    public void testSetSensitiveOnNotifRowUpdatesLayout() throws Exception {
+        // GIVEN a sensitive notification row that's currently redacted
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        measureAndLayout(row);
+        row.setHideSensitiveForIntrinsicHeight(true);
+        row.setSensitive(true, true);
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+
+        // GIVEN a mocked private layout
+        NotificationContentView mockPrivateLayout = mock(NotificationContentView.class);
+        row.setPrivateLayout(mockPrivateLayout);
+
+        // WHEN the row is set to no longer be sensitive
+        row.setSensitive(false, true);
+
+        // VERIFY that the layout is updated
+        assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
+        verify(mockPrivateLayout).requestSelectLayout(eq(true));
+    }
+
+    @Test
+    @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
     public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
         // GIVEN a sensitive notification row that's currently redacted
         ExpandableNotificationRow row = mNotificationTestHelper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 699e8c3..47238fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -23,6 +23,7 @@
 import android.testing.TestableLooper
 import android.testing.ViewUtils
 import android.view.NotificationHeaderView
+import android.view.NotificationTopLineView
 import android.view.View
 import android.view.ViewGroup
 import android.widget.FrameLayout
@@ -37,6 +38,7 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -82,8 +84,21 @@
         val mockEntry = createMockNotificationEntry()
         row =
             spy(
-                ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply {
-                    entry = mockEntry
+                when (NotificationBundleUi.isEnabled) {
+                    true -> {
+                        ExpandableNotificationRow(
+                            mContext,
+                            /* attrs= */ null,
+                            UserHandle.CURRENT
+                        ).apply {
+                            entry = mockEntry
+                        }
+                    }
+                    false -> {
+                        ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply {
+                            entry = mockEntry
+                        }
+                    }
                 }
             )
         ViewUtils.attachView(fakeParent)
@@ -270,7 +285,7 @@
         val icon =
             FeedbackIcon(
                 R.drawable.ic_feedback_alerted,
-                R.string.notification_feedback_indicator_alerted
+                R.string.notification_feedback_indicator_alerted,
             )
         view.setFeedbackIcon(icon)
 
@@ -291,10 +306,7 @@
         val mockHeadsUpEB = mock<NotificationExpandButton>()
         val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB)
 
-        val view =
-            createContentView(
-                isSystemExpanded = false,
-            )
+        val view = createContentView(isSystemExpanded = false)
 
         // Update all 3 child forms
         view.apply {
@@ -319,12 +331,14 @@
 
     private fun createMockNotificationHeaderView(
         height: Int,
-        mockExpandedEB: NotificationExpandButton
+        mockExpandedEB: NotificationExpandButton,
     ) =
         spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height })
             .apply {
                 whenever(this.animate()).thenReturn(mock())
                 whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+                whenever(this.findViewById<View>(R.id.notification_top_line))
+                    .thenReturn(mock<NotificationTopLineView>())
             }
 
     @Test
@@ -344,7 +358,7 @@
                 isSystemExpanded = false,
                 contractedView = mockContracted,
                 expandedView = mockExpanded,
-                headsUpView = mockHeadsUp
+                headsUpView = mockHeadsUp,
             )
 
         view.setRemoteInputVisible(true)
@@ -373,7 +387,7 @@
                 isSystemExpanded = false,
                 contractedView = mockContracted,
                 expandedView = mockExpanded,
-                headsUpView = mockHeadsUp
+                headsUpView = mockHeadsUp,
             )
 
         view.setRemoteInputVisible(false)
@@ -635,7 +649,7 @@
         contractedView: View = createViewWithHeight(contractedHeight),
         expandedView: View = createViewWithHeight(expandedHeight),
         headsUpView: View = createViewWithHeight(contractedHeight),
-        row: ExpandableNotificationRow = this.row
+        row: ExpandableNotificationRow = this.row,
     ): NotificationContentView {
         val height = if (isSystemExpanded) expandedHeight else contractedHeight
         doReturn(height).whenever(row).intrinsicHeight
@@ -647,7 +661,7 @@
                 setHeights(
                     /* smallHeight= */ contractedHeight,
                     /* headsUpMaxHeight= */ contractedHeight,
-                    /* maxHeight= */ expandedHeight
+                    /* maxHeight= */ expandedHeight,
                 )
                 contractedChild = contractedView
                 expandedChild = expandedView
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 14a1233..1088676 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -63,6 +63,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
@@ -118,10 +119,8 @@
     @Rule public Expect mExpect = Expect.create();
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
 
-    private final FakeConfigurationController mConfigurationController =
-            new FakeConfigurationController();
-    private final LargeScreenShadeInterpolator
-            mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+    private FakeConfigurationController mConfigurationController;
+    private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator;
 
     private final TestScope mTestScope = mKosmos.getTestScope();
     private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
@@ -137,6 +136,7 @@
     private boolean mAlwaysOnEnabled;
     private TestableLooper mLooper;
     private Context mContext;
+
     @Mock private DozeParameters mDozeParameters;
     @Mock private LightBarController mLightBarController;
     @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory;
@@ -149,12 +149,11 @@
     @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
     @Mock private AlternateBouncerToGoneTransitionViewModel
             mAlternateBouncerToGoneTransitionViewModel;
-    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
-            mKosmos.getKeyguardTransitionInteractor();
-    private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
-            mKosmos.getKeyguardTransitionRepository();
     @Mock private KeyguardInteractor mKeyguardInteractor;
 
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private FakeKeyguardTransitionRepository mKeyguardTransitionRepository;
+
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
     //   event-dispatch-on-registration pattern caused some of these unit tests to fail.)
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -238,6 +237,9 @@
         when(mContext.getColor(com.android.internal.R.color.materialColorSurface))
                 .thenAnswer(invocation -> mSurfaceColor);
 
+        mConfigurationController = new FakeConfigurationController();
+        mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+
         mScrimBehind = spy(new ScrimView(mContext));
         mScrimInFront = new ScrimView(mContext);
         mNotificationsScrim = new ScrimView(mContext);
@@ -270,6 +272,9 @@
         when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
                 .thenReturn(emptyFlow());
 
+        mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository();
+        mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor();
+
         mScrimController = new ScrimController(
                 mLightBarController,
                 mDozeParameters,
@@ -322,6 +327,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguard() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
@@ -337,6 +343,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToShadeLocked() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setQsPosition(1f, 0);
@@ -373,6 +380,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToShadeLocked_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -391,6 +399,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToOff() {
         mScrimController.legacyTransitionTo(ScrimState.OFF);
         finishAnimationsImmediately();
@@ -406,6 +415,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_withRegularWallpaper() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         finishAnimationsImmediately();
@@ -421,6 +431,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_withFrontAlphaUpdates() {
         // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -465,6 +476,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() {
         // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -506,6 +518,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToPulsing_withFrontAlphaUpdates() {
         // Pre-condition
         // Need to go to AoD first because PULSING doesn't change
@@ -551,6 +564,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguardBouncer() {
         mScrimController.legacyTransitionTo(BOUNCER);
         finishAnimationsImmediately();
@@ -571,6 +585,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void lockscreenToHubTransition_setsBehindScrimAlpha() {
         // Start on lockscreen.
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -617,6 +632,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void hubToLockscreenTransition_setsViewAlpha() {
         // Start on glanceable hub.
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
@@ -663,6 +679,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToHub() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -677,6 +694,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openBouncerOnHub() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
 
@@ -706,6 +724,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openShadeOnHub() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
 
@@ -734,6 +753,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToHubOverDream() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -748,6 +768,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openBouncerOnHubOverDream() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
 
@@ -777,6 +798,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void openShadeOnHubOverDream() {
         mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
 
@@ -805,6 +827,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
         assertEquals(BOUNCER.getBehindTint(), 0x112233);
         mSurfaceColor = 0x223344;
@@ -813,6 +836,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -825,6 +849,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToKeyguardBouncer_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -845,6 +870,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -867,6 +893,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(BOUNCER);
@@ -889,6 +916,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToBouncer() {
         mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED);
         finishAnimationsImmediately();
@@ -902,6 +930,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlocked_clippedQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.setRawPanelExpansionFraction(0f);
@@ -960,6 +989,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.setRawPanelExpansionFraction(0f);
@@ -999,6 +1029,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimStateCallback() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1014,6 +1045,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void panelExpansion() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1036,6 +1068,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion() {
         reset(mScrimBehind);
         mScrimController.setQsPosition(1f, 999 /* value doesn't matter */);
@@ -1048,6 +1081,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_clippingQs() {
         reset(mScrimBehind);
         mScrimController.setClipsQsScrim(true);
@@ -1061,6 +1095,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_half_clippingQs() {
         reset(mScrimBehind);
         mScrimController.setClipsQsScrim(true);
@@ -1074,6 +1109,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void panelExpansionAffectsAlpha() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1096,6 +1132,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlockedFromOff() {
         // Simulate unlock with fingerprint without AOD
         mScrimController.legacyTransitionTo(ScrimState.OFF);
@@ -1118,6 +1155,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToUnlockedFromAod() {
         // Simulate unlock with fingerprint
         mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1140,6 +1178,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimBlanksBeforeLeavingAod() {
         // Simulate unlock with fingerprint
         mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1163,6 +1202,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void scrimBlankCallbackWhenUnlockingFromPulse() {
         boolean[] blanked = {false};
         // Simulate unlock with fingerprint
@@ -1181,6 +1221,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void blankingNotRequired_leavingAoD() {
         // GIVEN display does NOT need blanking
         when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
@@ -1236,6 +1277,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallback() {
         int[] callOrder = {0, 0, 0};
         int[] currentCall = {0};
@@ -1262,12 +1304,14 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallbacksWithoutAmbientDisplay() {
         mAlwaysOnEnabled = false;
         testScrimCallback();
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimCallbackCancelled() {
         boolean[] cancelledCalled = {false};
         mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() {
@@ -1281,6 +1325,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testHoldsWakeLock_whenAOD() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         verify(mWakeLock).acquire(anyString());
@@ -1290,6 +1335,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoesNotHoldWakeLock_whenUnlocking() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1297,6 +1343,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testCallbackInvokedOnSameStateTransition() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
@@ -1306,6 +1353,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testConservesExpansionOpacityAfterTransition() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1323,6 +1371,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testCancelsOldAnimationBeforeBlanking() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         finishAnimationsImmediately();
@@ -1335,6 +1384,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsAreNotFocusable() {
         assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable());
         assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable());
@@ -1343,6 +1393,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testEatsTouchEvent() {
         HashSet<ScrimState> eatsTouches =
                 new HashSet<>(Collections.singletonList(ScrimState.AOD));
@@ -1359,6 +1410,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testAnimatesTransitionToAod() {
         when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
         ScrimState.AOD.prepare(ScrimState.KEYGUARD);
@@ -1373,6 +1425,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testIsLowPowerMode() {
         HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList(
                 ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING));
@@ -1390,6 +1443,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsOpaque_whenShadeFullyExpanded() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setRawPanelExpansionFraction(1);
@@ -1404,6 +1458,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisible() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1419,6 +1474,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoesntAnimate_whenUnlocking() {
         // LightRevealScrim will animate the transition, we should only hide the keyguard scrims.
         ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD);
@@ -1439,6 +1495,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisible_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1454,6 +1511,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testScrimsVisible_whenShadeVisibleOnLockscreen() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         mScrimController.setQsPosition(0.25f, 300);
@@ -1465,6 +1523,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationScrimTransparent_whenOnLockscreen() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         // even if shade is not pulled down, panel has expansion of 1 on the lockscreen
@@ -1477,6 +1536,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() {
         mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -1488,6 +1548,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         // clipping doesn't change tested logic but allows to assert scrims more in line with
@@ -1504,6 +1565,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
 
@@ -1520,6 +1582,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
@@ -1535,6 +1598,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
@@ -1554,6 +1618,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         mScrimController.setClipsQsScrim(true);
@@ -1574,6 +1639,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
         mScrimController.setClipsQsScrim(true);
@@ -1594,6 +1660,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() {
         when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
         mScrimController.setClipsQsScrim(false);
@@ -1605,6 +1672,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.setClipsQsScrim(true);
 
@@ -1646,6 +1714,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationTransparency_followsNotificationScrimProgress() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setRawPanelExpansionFraction(1.0f);
@@ -1662,6 +1731,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1697,6 +1767,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() {
         int overScrollAmount = 10;
 
@@ -1706,6 +1777,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() {
         int overScrollAmount = 10;
 
@@ -1715,6 +1787,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() {
         int overScrollAmount = 10;
 
@@ -1724,6 +1797,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationBoundsTopGetsPassedToKeyguard() {
         mScrimController.legacyTransitionTo(SHADE_LOCKED);
         mScrimController.setQsPosition(1f, 0);
@@ -1734,6 +1808,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() {
         mScrimController.setKeyguardOccluded(true);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1744,6 +1819,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void transitionToDreaming() {
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -1763,6 +1839,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void keyguardGoingAwayUpdateScrims() {
         when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
         mScrimController.updateScrims();
@@ -1772,6 +1849,7 @@
 
 
     @Test
+    @DisableSceneContainer
     public void setUnOccludingAnimationKeyguard() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
@@ -1786,6 +1864,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testHidesScrimFlickerInActivity() {
         mScrimController.setKeyguardOccluded(true);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1804,6 +1883,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1813,6 +1893,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void aodStateSetsFrontScrimToNotBlend() {
         mScrimController.legacyTransitionTo(ScrimState.AOD);
         assertFalse("Front scrim should not blend with main color",
@@ -1820,6 +1901,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void applyState_unlocked_bouncerShowing() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.setBouncerHiddenFraction(0.99f);
@@ -1829,6 +1911,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void ignoreTransitionRequestWhileKeyguardTransitionRunning() {
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1841,6 +1924,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() {
         when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1851,6 +1935,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void primaryBouncerToGoneOnFinishCallsLightBarController() {
         reset(mLightBarController);
         mScrimController.mBouncerToGoneTransition.accept(
@@ -1862,6 +1947,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testDoNotAnimateChangeIfOccludeAnimationPlaying() {
         mScrimController.setOccludeAnimationPlaying(true);
         mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1870,6 +1956,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() {
         mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
         when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true);
@@ -1942,9 +2029,9 @@
 
         // Check combined scrim visibility.
         final int visibility;
-        if (scrimToAlpha.values().contains(OPAQUE)) {
+        if (scrimToAlpha.containsValue(OPAQUE)) {
             visibility = OPAQUE;
-        } else if (scrimToAlpha.values().contains(SEMI_TRANSPARENT)) {
+        } else if (scrimToAlpha.containsValue(SEMI_TRANSPARENT)) {
             visibility = SEMI_TRANSPARENT;
         } else {
             visibility = TRANSPARENT;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
rename to packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8d05ea1..4439242 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -465,9 +465,9 @@
     public void notificationVolumeSeparated_theRingerIconChangesToSpeakerIcon() {
         // already separated. assert icon is new based on res id
         assertEquals(mDialog.mVolumeRingerIconDrawableId,
-                R.drawable.ic_speaker_on);
+                R.drawable.ic_legacy_speaker_on);
         assertEquals(mDialog.mVolumeRingerMuteIconDrawableId,
-                R.drawable.ic_speaker_mute);
+                R.drawable.ic_legacy_speaker_mute);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
index a6e7133..5dc28be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
@@ -17,33 +17,77 @@
 package com.android.systemui.activity.data.repository
 
 import android.app.activityManager
+import com.android.systemui.activity.data.model.AppVisibilityModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.log.core.Logger
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.fakeSystemClock
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
-val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() }
+val Kosmos.activityManagerRepository by
+    Kosmos.Fixture { FakeActivityManagerRepository(fakeSystemClock) }
 
 val Kosmos.realActivityManagerRepository by
-    Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) }
+    Kosmos.Fixture {
+        ActivityManagerRepositoryImpl(testDispatcher, fakeSystemClock, activityManager)
+    }
 
-class FakeActivityManagerRepository : ActivityManagerRepository {
-    private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>()
+class FakeActivityManagerRepository(private val systemClock: SystemClock) :
+    ActivityManagerRepository {
+    private val isVisibleFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>()
+    private val appVisibilityFlows =
+        mutableMapOf<Int, MutableList<MutableStateFlow<AppVisibilityModel>>>()
 
     var startingIsAppVisibleValue = false
 
+    override fun createAppVisibilityFlow(
+        creationUid: Int,
+        logger: Logger,
+        identifyingLogTag: String,
+    ): Flow<AppVisibilityModel> {
+        val newFlow =
+            MutableStateFlow(
+                if (startingIsAppVisibleValue) {
+                    AppVisibilityModel(
+                        isAppCurrentlyVisible = true,
+                        lastAppVisibleTime = systemClock.currentTimeMillis(),
+                    )
+                } else {
+                    AppVisibilityModel(isAppCurrentlyVisible = false, lastAppVisibleTime = null)
+                }
+            )
+        appVisibilityFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
+        return newFlow
+    }
+
     override fun createIsAppVisibleFlow(
         creationUid: Int,
         logger: Logger,
         identifyingLogTag: String,
     ): MutableStateFlow<Boolean> {
         val newFlow = MutableStateFlow(startingIsAppVisibleValue)
-        uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
+        isVisibleFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
         return newFlow
     }
 
     fun setIsAppVisible(uid: Int, isAppVisible: Boolean) {
-        uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible }
+        isVisibleFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible }
+        appVisibilityFlows[uid]?.forEach { stateFlow ->
+            stateFlow.value =
+                if (isAppVisible) {
+                    AppVisibilityModel(
+                        isAppCurrentlyVisible = true,
+                        lastAppVisibleTime = systemClock.currentTimeMillis(),
+                    )
+                } else {
+                    AppVisibilityModel(
+                        isAppCurrentlyVisible = false,
+                        stateFlow.value.lastAppVisibleTime,
+                    )
+                }
+        }
     }
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
index 60c0f34..f9917ac 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -44,6 +44,8 @@
     var pendingOverlays: Set<OverlayKey>? = null
         private set
 
+    var freezeAndAnimateToCurrentStateCallCount = 0
+
     override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
         if (_isPaused) {
             _pendingScene = toScene
@@ -85,6 +87,10 @@
         hideOverlay(overlay)
     }
 
+    override fun freezeAndAnimateToCurrentState() {
+        freezeAndAnimateToCurrentStateCallCount++
+    }
+
     /**
      * Pauses scene and overlay changes.
      *
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
index 67dd0ad..0892e66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
@@ -27,7 +27,7 @@
 
 val Kosmos.shadeDisplayChangeLatencyTracker by Fixture {
     ShadeDisplayChangeLatencyTracker(
-        Optional.of(mockShadeRootView),
+        mockShadeRootView,
         configurationRepository,
         latencyTracker,
         testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
index 4631413..1397d97 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.statusbar.notification.row.notificationRebindingTracker
 import com.android.systemui.statusbar.notification.stack.notificationStackRebindingHider
 import com.android.systemui.statusbar.policy.configurationController
-import java.util.Optional
 import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -55,11 +54,11 @@
             testScope.backgroundScope,
             testScope.backgroundScope.coroutineContext,
             mockedShadeDisplayChangeLatencyTracker,
-            Optional.of(shadeExpandedStateInteractor),
+            shadeExpandedStateInteractor,
             shadeExpansionIntent,
             activeNotificationsInteractor,
             notificationRebindingTracker,
-            Optional.of(notificationStackRebindingHider),
+            notificationStackRebindingHider,
             configurationController,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 20e4523..55e35f2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -18,9 +18,11 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
 
@@ -31,6 +33,8 @@
         notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
         sceneInteractor = sceneInteractor,
         shadeInteractor = shadeInteractor,
+        disableFlagsInteractor = disableFlagsInteractor,
+        mediaCarouselInteractor = mediaCarouselInteractor,
         activeNotificationsInteractor = activeNotificationsInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index 878c2de..d8e0cfe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.notifChipsViewModel: NotifChipsViewModel by
     Kosmos.Fixture {
@@ -29,5 +30,6 @@
             applicationCoroutineScope,
             statusBarNotificationChipsInteractor,
             headsUpNotificationInteractor,
+            fakeSystemClock,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt
index 93502f3..b876095 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt
@@ -17,13 +17,14 @@
 package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModel
 
-val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by
+private val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by
+    Kosmos.Fixture { StatusBarPopupChipsViewModel(mediaControlChip = mediaControlChipViewModel) }
+
+val Kosmos.statusBarPopupChipsViewModelFactory by
     Kosmos.Fixture {
-        StatusBarPopupChipsViewModel(
-            testScope.backgroundScope,
-            mediaControlChipViewModel = mediaControlChipViewModel,
-        )
+        object : StatusBarPopupChipsViewModel.Factory {
+            override fun create(): StatusBarPopupChipsViewModel = statusBarPopupChipsViewModel
+        }
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt
new file mode 100644
index 0000000..59f5ecd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2025 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.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.content.applicationContext
+import android.graphics.drawable.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.icon.IconPack
+import com.android.systemui.statusbar.notification.promoted.setPromotedContent
+import org.mockito.kotlin.mock
+
+fun Kosmos.setIconPackWithMockIconViews(entry: NotificationEntry) {
+    entry.icons =
+        IconPack.buildPack(
+            /* statusBarIcon = */ mock(),
+            /* statusBarChipIcon = */ mock(),
+            /* shelfIcon = */ mock(),
+            /* aodIcon = */ mock(),
+            /* source = */ null,
+        )
+}
+
+fun Kosmos.buildOngoingCallEntry(
+    promoted: Boolean = false,
+    block: NotificationEntryBuilder.() -> Unit = {},
+): NotificationEntry =
+    buildNotificationEntry(
+        tag = "call",
+        promoted = promoted,
+        style = makeOngoingCallStyle(),
+        block = block,
+    )
+
+fun Kosmos.buildPromotedOngoingEntry(
+    block: NotificationEntryBuilder.() -> Unit = {}
+): NotificationEntry =
+    buildNotificationEntry(tag = "ron", promoted = true, style = null, block = block)
+
+fun Kosmos.buildNotificationEntry(
+    tag: String? = null,
+    promoted: Boolean = false,
+    style: Notification.Style? = null,
+    block: NotificationEntryBuilder.() -> Unit = {},
+): NotificationEntry =
+    NotificationEntryBuilder()
+        .apply {
+            setTag(tag)
+            setFlag(applicationContext, Notification.FLAG_PROMOTED_ONGOING, promoted)
+            modifyNotification(applicationContext)
+                .setSmallIcon(Icon.createWithContentUri("content://null"))
+                .setStyle(style)
+        }
+        .apply(block)
+        .build()
+        .also {
+            setIconPackWithMockIconViews(it)
+            if (promoted) setPromotedContent(it)
+        }
+
+private fun Kosmos.makeOngoingCallStyle(): Notification.CallStyle {
+    val pendingIntent =
+        PendingIntent.getBroadcast(
+            applicationContext,
+            0,
+            Intent("action"),
+            PendingIntent.FLAG_IMMUTABLE,
+        )
+    val person = Person.Builder().setName("person").build()
+    return Notification.CallStyle.forOngoingCall(person, pendingIntent)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
index a48b270..fa3702ce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.collection
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
 
-var Kosmos.notifPipeline by Kosmos.Fixture { mock<NotifPipeline>() }
+var Kosmos.notifPipeline by Kosmos.Fixture { mockNotifPipeline }
+var Kosmos.mockNotifPipeline by Kosmos.Fixture { mock<NotifPipeline>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
index 63521de..e55cd0dc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
@@ -16,8 +16,11 @@
 
 package com.android.systemui.statusbar.notification.promoted
 
+import android.app.Notification
 import android.content.applicationContext
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.RowImageInflater
 import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform
 
 var Kosmos.promotedNotificationContentExtractor by
@@ -28,3 +31,14 @@
             promotedNotificationLogger,
         )
     }
+
+fun Kosmos.setPromotedContent(entry: NotificationEntry) {
+    val extractedContent =
+        promotedNotificationContentExtractor.extractContent(
+            entry,
+            Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
+            RowImageInflater.newInstance(null).useForContentModel(),
+        )
+    entry.promotedNotificationContentModel =
+        requireNotNull(extractedContent) { "extractContent returned null" }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
index df1c822..fcd4843 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
@@ -18,12 +18,11 @@
 
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 
 val Kosmos.aodPromotedNotificationInteractor by
     Kosmos.Fixture {
         AODPromotedNotificationInteractor(
-            activeNotificationsInteractor = activeNotificationsInteractor,
+            promotedNotificationsInteractor = promotedNotificationsInteractor,
             dumpManager = dumpManager,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt
new file mode 100644
index 0000000..093ec10
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 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.notification.promoted.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+
+val Kosmos.promotedNotificationsInteractor by
+    Kosmos.Fixture {
+        PromotedNotificationsInteractor(
+            activeNotificationsInteractor = activeNotificationsInteractor,
+            callChipInteractor = callChipInteractor,
+            notifChipsInteractor = statusBarNotificationChipsInteractor,
+            backgroundDispatcher = testDispatcher,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index e445a73..8b19491 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.system.ActivityManagerWrapper
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper
 import com.android.systemui.shared.system.PackageManagerWrapper
@@ -346,10 +347,15 @@
         // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
         //  set, but we do not want to override an existing value that is needed by a specific test.
 
+        val userTracker = Mockito.mock(UserTracker::class.java, STUB_ONLY)
+        whenever(userTracker.userHandle).thenReturn(context.user)
+
         val rowInflaterTask =
             RowInflaterTask(
                 mFakeSystemClock,
                 Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY),
+                userTracker,
+                Mockito.mock(AsyncRowInflater::class.java, STUB_ONLY),
             )
         val row = rowInflaterTask.inflateSynchronously(context, null, entry)
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index bc1363a..970b87c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -33,7 +33,7 @@
 
 val Kosmos.notificationListViewBinder by Fixture {
     NotificationListViewBinder(
-        backgroundDispatcher = testDispatcher,
+        inflationDispatcher = testDispatcher,
         hiderTracker = displaySwitchNotificationsHiderTracker,
         configuration = configurationState,
         falsingManager = falsingManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 047bd13..7a2b7c2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel
-import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel
@@ -82,7 +81,6 @@
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
         aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel,
-        dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel,
         dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
         dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index fbada93..a97c651 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor
-import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel
+import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModelFactory
 import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -59,7 +59,7 @@
             shadeInteractor,
             shareToAppChipViewModel,
             ongoingActivityChipsViewModel,
-            statusBarPopupChipsViewModel,
+            statusBarPopupChipsViewModelFactory,
             systemStatusEventAnimationInteractor,
             multiDisplayStatusBarContentInsetsViewModelStore,
             backgroundScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt
new file mode 100644
index 0000000..81f71e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 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.util.settings.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.settings.repository.SecureSettingsForUserRepository
+
+val Kosmos.secureSettingsForUserRepository by
+    Kosmos.Fixture {
+        SecureSettingsForUserRepository(fakeSettings, testDispatcher, backgroundCoroutineContext)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
index 703d6ad..a209ec9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
@@ -31,3 +31,6 @@
     }
 
 val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
+
+val SystemClock.fake
+    get() = this as FakeSystemClock
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
index 96bc972..8c8d024 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
 
-import android.content.applicationContext
 import com.android.internal.logging.uiEventLogger
 import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.volume.domain.interactor.audioSharingInteractor
+import com.android.systemui.volume.shared.volumePanelLogger
 import kotlinx.coroutines.CoroutineScope
 
 val Kosmos.audioSharingStreamSliderViewModelFactory by
@@ -29,10 +29,10 @@
             override fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel {
                 return AudioSharingStreamSliderViewModel(
                     coroutineScope,
-                    applicationContext,
                     audioSharingInteractor,
                     uiEventLogger,
                     sliderHapticsViewModelFactory,
+                    volumePanelLogger,
                 )
             }
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
index abd4235..6875619 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.volume.mediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.shared.volumePanelLogger
 import kotlinx.coroutines.CoroutineScope
 
 val Kosmos.castVolumeSliderViewModelFactory by
@@ -36,6 +37,7 @@
                     applicationContext,
                     mediaDeviceSessionInteractor,
                     sliderHapticsViewModelFactory,
+                    volumePanelLogger,
                 )
             }
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
index aeff86e..24d2f1f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
@@ -34,12 +34,15 @@
         _wallpaperFocalAreaBounds.asStateFlow()
 
     private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F))
-    override val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
+    val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
         _wallpaperFocalAreaTapPosition.asStateFlow()
 
     private val _notificationDefaultTop = MutableStateFlow(0F)
     override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow()
 
+    private val _hasFocalArea = MutableStateFlow(false)
+    override val hasFocalArea: StateFlow<Boolean> = _hasFocalArea.asStateFlow()
+
     override fun setShortcutAbsoluteTop(top: Float) {
         _shortcutAbsoluteTop.value = top
     }
@@ -56,7 +59,7 @@
         _wallpaperFocalAreaBounds.value = bounds
     }
 
-    override fun setTapPosition(point: PointF) {
-        _wallpaperFocalAreaTapPosition.value = point
+    override fun setTapPosition(tapPosition: PointF) {
+        _wallpaperFocalAreaTapPosition.value = tapPosition
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index 8689e04..66bb803 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.wallpapers.data.repository
 
 import android.app.WallpaperInfo
+import android.graphics.PointF
+import android.graphics.RectF
 import android.view.View
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -34,9 +36,9 @@
     private val _shouldSendFocalArea = MutableStateFlow(false)
     override val shouldSendFocalArea: StateFlow<Boolean> = _shouldSendFocalArea.asStateFlow()
 
-    fun setShouldSendFocalArea(shouldSendFocalArea: Boolean) {
-        _shouldSendFocalArea.value = shouldSendFocalArea
-    }
+    override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {}
+
+    override fun sendTapCommand(tapPosition: PointF) {}
 
     fun setWallpaperInfo(wallpaperInfo: WallpaperInfo?) {
         _wallpaperInfo.value = wallpaperInfo
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
index 7ebec6c..1761503 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
@@ -19,7 +19,6 @@
 import android.content.applicationContext
 import com.android.app.wallpaperManager
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
@@ -34,8 +33,6 @@
         bgDispatcher = testDispatcher,
         broadcastDispatcher = broadcastDispatcher,
         userRepository = userRepository,
-        keyguardTransitionInteractor = keyguardTransitionInteractor,
-        wallpaperFocalAreaRepository = wallpaperFocalAreaRepository,
         wallpaperManager = wallpaperManager,
         secureSettings = fakeSettings,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
index 88eb551..eaf55a7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
@@ -18,20 +18,14 @@
 
 import android.content.applicationContext
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.wallpaperRepository
 
-val Kosmos.wallpaperFocalAreaInteractor by
+var Kosmos.wallpaperFocalAreaInteractor by
     Kosmos.Fixture {
         WallpaperFocalAreaInteractor(
-            applicationScope = applicationCoroutineScope,
             context = applicationContext,
             wallpaperFocalAreaRepository = wallpaperFocalAreaRepository,
             shadeRepository = shadeRepository,
-            activeNotificationsInteractor = activeNotificationsInteractor,
-            wallpaperRepository = wallpaperRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
index 7e232c5..4032503 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
@@ -16,10 +16,14 @@
 
 package com.android.systemui.wallpapers.ui.viewmodel
 
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor
 
 var Kosmos.wallpaperFocalAreaViewModel by
     Kosmos.Fixture {
-        WallpaperFocalAreaViewModel(wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor)
+        WallpaperFocalAreaViewModel(
+            wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor,
+            keyguardTransitionInteractor = keyguardTransitionInteractor,
+        )
     }
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodMethodCallLogger.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodMethodCallLogger.java
new file mode 100644
index 0000000..7ee9d7a
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodMethodCallLogger.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2025 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 android.platform.test.ravenwood;
+
+import com.android.ravenwood.RavenwoodRuntimeNative;
+
+import java.io.PrintStream;
+import java.util.HashSet;
+import java.util.Objects;
+
+/**
+ * Provides a method call hook that prints almost all (see below) the framework methods being
+ * called with indentation.
+ *
+ * We don't log methods that are trivial, uninteresting, or would be too noisy.
+ * e.g. we don't want to log any logging related methods, or collection APIs.
+ *
+ */
+public class RavenwoodMethodCallLogger {
+    private RavenwoodMethodCallLogger() {
+    }
+
+    /** We don't want to log anything before ravenwood is initialized. This flag controls it.*/
+    private static volatile boolean sEnabled = false;
+
+    private static volatile PrintStream sOut = System.out;
+
+    /** Return the current thread's call nest level. */
+    private static int getNestLevel() {
+        return Thread.currentThread().getStackTrace().length;
+    }
+
+    private static class ThreadInfo {
+        /**
+         * We save the current thread's nest call level here and use that as the initial level.
+         * We do it because otherwise the nest level would be too deep by the time test
+         * starts.
+         */
+        public final int mInitialNestLevel = getNestLevel();
+
+        /**
+         * A nest level where shouldLog() returned false.
+         * Once it's set, we ignore all calls deeper than this.
+         */
+        public int mDisabledNestLevel = Integer.MAX_VALUE;
+    }
+
+    private static final ThreadLocal<ThreadInfo> sThreadInfo = new ThreadLocal<>() {
+        @Override
+        protected ThreadInfo initialValue() {
+            return new ThreadInfo();
+        }
+    };
+
+    /** Classes that should be logged. Uses a map for fast lookup. */
+    private static final HashSet<Class> sIgnoreClasses = new HashSet<>();
+    static {
+        // The following classes are not interesting...
+        sIgnoreClasses.add(android.util.Log.class);
+        sIgnoreClasses.add(android.util.Slog.class);
+        sIgnoreClasses.add(android.util.EventLog.class);
+        sIgnoreClasses.add(android.util.TimingsTraceLog.class);
+
+        sIgnoreClasses.add(android.util.SparseArray.class);
+        sIgnoreClasses.add(android.util.SparseIntArray.class);
+        sIgnoreClasses.add(android.util.SparseLongArray.class);
+        sIgnoreClasses.add(android.util.SparseBooleanArray.class);
+        sIgnoreClasses.add(android.util.SparseDoubleArray.class);
+        sIgnoreClasses.add(android.util.SparseSetArray.class);
+        sIgnoreClasses.add(android.util.SparseArrayMap.class);
+        sIgnoreClasses.add(android.util.LongSparseArray.class);
+        sIgnoreClasses.add(android.util.LongSparseLongArray.class);
+        sIgnoreClasses.add(android.util.LongArray.class);
+
+        sIgnoreClasses.add(android.text.FontConfig.class);
+
+        sIgnoreClasses.add(android.os.SystemClock.class);
+        sIgnoreClasses.add(android.os.Trace.class);
+        sIgnoreClasses.add(android.os.LocaleList.class);
+        sIgnoreClasses.add(android.os.Build.class);
+        sIgnoreClasses.add(android.os.SystemProperties.class);
+
+        sIgnoreClasses.add(com.android.internal.util.Preconditions.class);
+
+        sIgnoreClasses.add(android.graphics.FontListParser.class);
+        sIgnoreClasses.add(android.graphics.ColorSpace.class);
+
+        sIgnoreClasses.add(android.graphics.fonts.FontStyle.class);
+        sIgnoreClasses.add(android.graphics.fonts.FontVariationAxis.class);
+
+        sIgnoreClasses.add(com.android.internal.compat.CompatibilityChangeInfo.class);
+        sIgnoreClasses.add(com.android.internal.os.LoggingPrintStream.class);
+
+        sIgnoreClasses.add(android.os.ThreadLocalWorkSource.class);
+
+        // Following classes *may* be interesting for some purposes, but the initialization is
+        // too noisy...
+        sIgnoreClasses.add(android.graphics.fonts.SystemFonts.class);
+
+    }
+
+    /**
+     * Return if a class should be ignored. Uses {link #sIgnoreCladsses}, but
+     * we ignore more classes.
+     */
+    private static boolean shouldIgnoreClass(Class<?> clazz) {
+        if (sIgnoreClasses.contains(clazz)) {
+            return true;
+        }
+        // Let's also ignore collection-ish classes in android.util.
+        if (java.util.Collection.class.isAssignableFrom(clazz)
+                || java.util.Map.class.isAssignableFrom(clazz)
+        ) {
+            if ("android.util".equals(clazz.getPackageName())) {
+                return true;
+            }
+            return false;
+        }
+
+        switch (clazz.getSimpleName()) {
+            case "EventLogTags":
+                return false;
+        }
+
+        // Following are classes that can't be referred to here directly.
+        // e.g. AndroidPrintStream is package-private, so we can't use its "class" here.
+        switch (clazz.getName()) {
+            case "com.android.internal.os.AndroidPrintStream":
+                return false;
+        }
+        return false;
+    }
+
+    private static boolean shouldLog(
+            Class<?> methodClass,
+            String methodName,
+            @SuppressWarnings("UnusedVariable") String methodDescriptor
+    ) {
+        // Should we ignore this class?
+        if (shouldIgnoreClass(methodClass)) {
+            return false;
+        }
+        // Is it a nested class in a class that should be ignored?
+        var host = methodClass.getNestHost();
+        if (host != methodClass && shouldIgnoreClass(host)) {
+            return false;
+        }
+
+        var className = methodClass.getName();
+
+        // Ad-hoc ignore list. They'd be too noisy.
+        if ("create".equals(methodName)
+                // We may apply jarjar, so use endsWith().
+                && className.endsWith("com.android.server.compat.CompatConfig")) {
+            return false;
+        }
+
+        var pkg = methodClass.getPackageName();
+        if (pkg.startsWith("android.icu")) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Call this to enable logging.
+     */
+    public static void enable(PrintStream out) {
+        sEnabled = true;
+        sOut = Objects.requireNonNull(out);
+
+        // It's called from the test thread (Java's main thread). Because we're already
+        // in deep nest calls, we initialize the initial nest level here.
+        sThreadInfo.get();
+    }
+
+    /** Actual method hook entry point.*/
+    public static void logMethodCall(
+            Class<?> methodClass,
+            String methodName,
+            String methodDescriptor
+    ) {
+        if (!sEnabled) {
+            return;
+        }
+        final var ti = sThreadInfo.get();
+        final int nestLevel = getNestLevel() - ti.mInitialNestLevel;
+
+        // Once shouldLog() returns false, we just ignore all deeper calls.
+        if (ti.mDisabledNestLevel < nestLevel) {
+            return; // Still ignore.
+        }
+        final boolean shouldLog = shouldLog(methodClass, methodName, methodDescriptor);
+
+        if (!shouldLog) {
+            ti.mDisabledNestLevel = nestLevel;
+            return;
+        }
+        ti.mDisabledNestLevel = Integer.MAX_VALUE;
+
+        var out = sOut;
+        out.print("# [");
+        out.print(RavenwoodRuntimeNative.gettid());
+        out.print(": ");
+        out.print(Thread.currentThread().getName());
+        out.print("]: ");
+        out.print("[@");
+        out.printf("%2d", nestLevel);
+        out.print("] ");
+        for (int i = 0; i < nestLevel; i++) {
+            out.print("  ");
+        }
+        out.println(methodClass.getName() + "." + methodName + methodDescriptor);
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 7af03ed..ae88bb2 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -23,7 +23,6 @@
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_RESOURCE_APK;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
-import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.parseNullableInt;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.withDefault;
@@ -103,6 +102,10 @@
     private RavenwoodRuntimeEnvironmentController() {
     }
 
+    private static final PrintStream sStdOut = System.out;
+    @SuppressWarnings("UnusedVariable")
+    private static final PrintStream sStdErr = System.err;
+
     private static final String MAIN_THREAD_NAME = "RavenwoodMain";
     private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
@@ -212,9 +215,9 @@
     }
 
     private static void globalInitInner() throws IOException {
-        if (RAVENWOOD_VERBOSE_LOGGING) {
-            Log.v(TAG, "globalInit() called here...", new RuntimeException("NOT A CRASH"));
-        }
+        // We haven't initialized liblog yet, so directly write to System.out here.
+        RavenwoodCommonUtils.log(TAG, "globalInitInner()");
+
         if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
             Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
         }
@@ -234,9 +237,6 @@
         dumpJavaProperties();
         dumpOtherInfo();
 
-        // We haven't initialized liblog yet, so directly write to System.out here.
-        RavenwoodCommonUtils.log(TAG, "globalInitInner()");
-
         // Make sure libravenwood_runtime is loaded.
         System.load(RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_RUNTIME_NAME));
 
@@ -261,6 +261,9 @@
         // Make sure libandroid_runtime is loaded.
         RavenwoodNativeLoader.loadFrameworkNativeCode();
 
+        // Start method logging.
+        RavenwoodMethodCallLogger.enable(sStdOut);
+
         // Touch some references early to ensure they're <clinit>'ed
         Objects.requireNonNull(Build.TYPE);
         Objects.requireNonNull(Build.VERSION.SDK);
diff --git a/ravenwood/runtime-jni/ravenwood_initializer.cpp b/ravenwood/runtime-jni/ravenwood_initializer.cpp
index 391c5d5..8a35ade 100644
--- a/ravenwood/runtime-jni/ravenwood_initializer.cpp
+++ b/ravenwood/runtime-jni/ravenwood_initializer.cpp
@@ -26,6 +26,10 @@
 #include <fcntl.h>
 
 #include <set>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <cstdlib>
 
 #include "jni_helper.h"
 
@@ -182,17 +186,82 @@
     }
 }
 
+// Find the PPID of child_pid using /proc/N/stat. The 4th field is the PPID.
+// Also returns child_pid's process name (2nd field).
+static pid_t getppid_of(pid_t child_pid, std::string& out_process_name) {
+    if (child_pid < 0) {
+        return -1;
+    }
+    std::string stat_file = "/proc/" + std::to_string(child_pid) + "/stat";
+    std::ifstream stat_stream(stat_file);
+    if (!stat_stream.is_open()) {
+        ALOGW("Unable to open '%s': %s", stat_file.c_str(), strerror(errno));
+        return -1;
+    }
+
+    std::string field;
+    int field_count = 0;
+    while (std::getline(stat_stream, field, ' ')) {
+        if (++field_count == 4) {
+            return atoi(field.c_str());
+        }
+        if (field_count == 2) {
+            out_process_name = field;
+        }
+    }
+    ALOGW("Unexpected format in '%s'", stat_file.c_str());
+    return -1;
+}
+
+// Find atest's PID. Climb up the process tree, and find "atest-py3".
+static pid_t find_atest_pid() {
+    auto ret = getpid(); // self (isolation runner process)
+
+    while (ret != -1) {
+        std::string proc;
+        ret = getppid_of(ret, proc);
+        if (proc == "(atest-py3)") {
+            return ret;
+        }
+    }
+
+    return ret;
+}
+
+// If $RAVENWOOD_LOG_OUT is set, redirect stdout/err to this file.
+// Originally it was added to allow to monitor log in realtime, with
+// RAVENWOOD_LOG_OUT=$(tty) atest...
+//
+// As a special case, if $RAVENWOOD_LOG_OUT is set to "-", we try to find
+// atest's process and send the output to its stdout. It's sort of hacky, but
+// this allows shell redirection to work on Ravenwood output too,
+// so e.g. `atest ... |tee atest.log` would work on Ravenwood's output.
+// (which wouldn't work with `RAVENWOOD_LOG_OUT=$(tty)`).
+//
+// Otherwise -- if $RAVENWOOD_LOG_OUT isn't set -- atest/tradefed just writes
+// the test's output to its own log file.
 static void maybeRedirectLog() {
     auto ravenwoodLogOut = getenv("RAVENWOOD_LOG_OUT");
-    if (ravenwoodLogOut == NULL) {
+    if (ravenwoodLogOut == NULL || *ravenwoodLogOut == '\0') {
         return;
     }
-    ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to %s", ravenwoodLogOut);
+    std::string path;
+    if (strcmp("-", ravenwoodLogOut) == 0) {
+        pid_t ppid = find_atest_pid();
+        if (ppid < 0) {
+            ALOGI("RAVENWOOD_LOG_OUT set to '-', but unable to find atest's PID");
+            return;
+        }
+        path = std::format("/proc/{}/fd/1", ppid);
+    } else {
+        path = ravenwoodLogOut;
+    }
+    ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to '%s'", path.c_str());
 
     // Redirect stdin / stdout to /dev/tty.
-    int ttyFd = open(ravenwoodLogOut, O_WRONLY | O_APPEND);
+    int ttyFd = open(path.c_str(), O_WRONLY | O_APPEND);
     if (ttyFd == -1) {
-        ALOGW("$RAVENWOOD_LOG_OUT is set to %s, but failed to open: %s ", ravenwoodLogOut,
+        ALOGW("$RAVENWOOD_LOG_OUT is set, but failed to open '%s': %s ", path.c_str(),
                 strerror(errno));
         return;
     }
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 91fd928..0edc348 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -9,8 +9,10 @@
 
 # Uncomment below lines to enable each feature.
 
+# Enable method call hook.
 #--default-method-call-hook
-#    com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+#    android.platform.test.ravenwood.RavenwoodMethodCallLogger.logMethodCall
+
 #--default-class-load-hook
 #    com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
 
diff --git a/ravenwood/tools/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/ravenwood/tools/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
index 78fd8f7..145325c 100644
--- a/ravenwood/tools/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
+++ b/ravenwood/tools/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
@@ -18,6 +18,7 @@
 import java.io.PrintStream;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Arrays;
 
 /**
  * Utilities used in the host side test environment.
@@ -36,9 +37,14 @@
 
     public static final String CLASS_INTERNAL_NAME = getInternalName(HostTestUtils.class);
 
+    /** If true, we skip all method call hooks */
+    private static final boolean SKIP_METHOD_CALL_HOOK = "1".equals(System.getenv(
+            "HOSTTEST_SKIP_METHOD_CALL_HOOK"));
+
     /** If true, we won't print method call log. */
-    private static final boolean SKIP_METHOD_LOG = "1".equals(System.getenv(
-            "HOSTTEST_SKIP_METHOD_LOG"));
+    private static final boolean SKIP_METHOD_LOG =
+            "1".equals(System.getenv("HOSTTEST_SKIP_METHOD_LOG"))
+            || "1".equals(System.getenv("RAVENWOOD_NO_METHOD_LOG"));
 
     /** If true, we won't print class load log. */
     private static final boolean SKIP_CLASS_LOG = "1".equals(System.getenv(
@@ -65,6 +71,9 @@
                         + "consider using Mockito; more details at go/ravenwood-docs");
     }
 
+    private static final Class<?>[] sMethodHookArgTypes =
+            { Class.class, String.class, String.class};
+
     /**
      * Trampoline method for method-call-hook.
      */
@@ -74,16 +83,22 @@
             String methodDescriptor,
             String callbackMethod
     ) {
-        callStaticMethodByName(callbackMethod, "method call hook", methodClass,
-                methodName, methodDescriptor);
+        if (SKIP_METHOD_CALL_HOOK) {
+            return;
+        }
+        callStaticMethodByName(callbackMethod, "method call hook", sMethodHookArgTypes,
+                methodClass, methodName, methodDescriptor);
     }
 
     /**
+     * Simple implementation of method call hook, which just prints the information of the
+     * method. This is just for basic testing. We don't use it in Ravenwood, because this would
+     * be way too noisy as it prints every single method, even trivial ones. (iterator methods,
+     * etc..)
+     *
      * I can be used as
      * {@code --default-method-call-hook
      * com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall}.
-     *
-     * It logs every single methods called.
      */
     public static void logMethodCall(
             Class<?> methodClass,
@@ -97,6 +112,8 @@
                 + methodName + methodDescriptor);
     }
 
+    private static final Class<?>[] sClassLoadHookArgTypes = { Class.class };
+
     /**
      * Called when any top level class (not nested classes) in the impl jar is loaded.
      *
@@ -111,11 +128,12 @@
         logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName()
                 + " calling hook " + callbackMethod);
 
-        callStaticMethodByName(callbackMethod, "class load hook", loadedClass);
+        callStaticMethodByName(
+                callbackMethod, "class load hook", sClassLoadHookArgTypes, loadedClass);
     }
 
     private static void callStaticMethodByName(String classAndMethodName,
-            String description, Object... args) {
+            String description, Class<?>[] argTypes, Object... args) {
         // Forward the call to callbackMethod.
         final int lastPeriod = classAndMethodName.lastIndexOf(".");
 
@@ -145,19 +163,14 @@
                     className));
         }
 
-        Class<?>[] argTypes = new Class[args.length];
-        for (int i = 0; i < args.length; i++) {
-            argTypes[i] = args[i].getClass();
-        }
-
         Method method = null;
         try {
             method = clazz.getMethod(methodName, argTypes);
         } catch (Exception e) {
             throw new HostTestException(String.format(
                     "Unable to find %s: class %s doesn't have method %s"
-                            + " (method must take exactly one parameter of type Class,"
-                            + " and public static)",
+                    + " Method must be public static, and arg types must be: "
+                    + Arrays.toString(argTypes),
                     description, className, methodName), e);
         }
         if (!(Modifier.isPublic(method.getModifiers())
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 9859475..3340990 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -145,6 +145,7 @@
 
         // Inject default hooks from options.
         filter = DefaultHookInjectingFilter(
+            allClasses,
             options.defaultClassLoadHook.get,
             options.defaultMethodCallHook.get,
             filter
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/DefaultHookInjectingFilter.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/DefaultHookInjectingFilter.kt
index d771003..aaf49c1 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/DefaultHookInjectingFilter.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/DefaultHookInjectingFilter.kt
@@ -16,8 +16,11 @@
 package com.android.hoststubgen.filters
 
 import com.android.hoststubgen.addLists
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.isAnnotation
 
 class DefaultHookInjectingFilter(
+    val classes: ClassNodes,
     defaultClassLoadHook: String?,
     defaultMethodCallHook: String?,
     fallback: OutputFilter
@@ -36,8 +39,30 @@
     private val defaultClassLoadHookAsList: List<String> = toSingleList(defaultClassLoadHook)
     private val defaultMethodCallHookAsList: List<String> = toSingleList(defaultMethodCallHook)
 
+    private fun shouldInject(className: String): Boolean {
+        // Let's not inject default hooks to annotation classes or inner classes of an annotation
+        // class, because these methods could be called at the class load time, which
+        // is very confusing, and usually not useful.
+
+        val cn = classes.findClass(className) ?: return false
+        if (cn.isAnnotation()) {
+            return false
+        }
+        cn.nestHostClass?.let { nestHostClass ->
+            val nestHost = classes.findClass(nestHostClass) ?: return false
+            if (nestHost.isAnnotation()) {
+                return false
+            }
+        }
+        return true
+    }
+
     override fun getClassLoadHooks(className: String): List<String> {
-        return addLists(super.getClassLoadHooks(className), defaultClassLoadHookAsList)
+        val s = super.getClassLoadHooks(className)
+        if (!shouldInject(className)) {
+            return s
+        }
+        return addLists(s, defaultClassLoadHookAsList)
     }
 
     override fun getMethodCallHooks(
@@ -45,9 +70,23 @@
         methodName: String,
         descriptor: String
     ): List<String> {
-        return addLists(
-            super.getMethodCallHooks(className, methodName, descriptor),
-            defaultMethodCallHookAsList,
-            )
+        val s = super.getMethodCallHooks(className, methodName, descriptor)
+        if (!shouldInject(className)) {
+            return s
+        }
+        // Don't hook Object methods.
+        if (methodName == "finalize" && descriptor == "()V") {
+            return s
+        }
+        if (methodName == "toString" && descriptor == "()Ljava/lang/String;") {
+            return s
+        }
+        if (methodName == "equals" && descriptor == "(Ljava/lang/Object;)Z") {
+            return s
+        }
+        if (methodName == "hashCode" && descriptor == "()I") {
+            return s
+        }
+        return addLists(s, defaultMethodCallHookAsList)
     }
-}
\ No newline at end of file
+}
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output.RELEASE_TARGET_JAVA_21/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output.RELEASE_TARGET_JAVA_21/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 49769e6..fb225ff 100644
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output.RELEASE_TARGET_JAVA_21/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output.RELEASE_TARGET_JAVA_21/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -6,17 +6,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestClassLoadHook
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestClassLoadHook
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String value();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -44,16 +36,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestKeep.java"
 RuntimeVisibleAnnotations:
@@ -75,16 +60,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRedirect
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirect
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestRedirect.java"
 RuntimeVisibleAnnotations:
@@ -106,17 +84,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRedirectionClass
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirectionClass
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String value();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -144,16 +114,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRemove
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRemove
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestRemove.java"
 RuntimeVisibleAnnotations:
@@ -175,16 +138,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestStaticInitializerKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestStaticInitializerKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestStaticInitializerKeep.java"
 RuntimeVisibleAnnotations:
@@ -206,17 +162,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestSubstitute
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestSubstitute
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String suffix();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -244,16 +192,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestThrow
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestThrow
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestThrow.java"
 RuntimeVisibleAnnotations:
@@ -275,16 +216,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestWholeClassKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestWholeClassKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestWholeClassKeep.java"
 RuntimeVisibleAnnotations:
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 0f8af92..e4b9db2 100644
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -6,17 +6,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestClassLoadHook
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestClassLoadHook
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String value();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -44,16 +36,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestKeep.java"
 RuntimeVisibleAnnotations:
@@ -75,16 +60,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRedirect
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirect
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestRedirect.java"
 RuntimeVisibleAnnotations:
@@ -106,17 +84,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRedirectionClass
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirectionClass
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String value();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -144,16 +114,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestRemove
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRemove
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestRemove.java"
 RuntimeVisibleAnnotations:
@@ -175,16 +138,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestStaticInitializerKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestStaticInitializerKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestStaticInitializerKeep.java"
 RuntimeVisibleAnnotations:
@@ -206,17 +162,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestSubstitute
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 2, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestSubstitute
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
-
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   public abstract java.lang.String suffix();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
@@ -244,16 +192,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestThrow
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestThrow
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestThrow.java"
 RuntimeVisibleAnnotations:
@@ -275,16 +216,9 @@
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
   this_class: #x                          // android/hosttest/annotation/HostSideTestWholeClassKeep
   super_class: #x                         // java/lang/Object
-  interfaces: 1, fields: 0, methods: 1, attributes: 2
-  private static {};
-    descriptor: ()V
-    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
-    Code:
-      stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestWholeClassKeep
-         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         x: return
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+Constant pool:
+{
 }
 SourceFile: "HostSideTestWholeClassKeep.java"
 RuntimeVisibleAnnotations:
@@ -307,6 +241,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/IPretendingAidl$Stub$Proxy
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -377,6 +313,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/IPretendingAidl$Stub
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -447,6 +385,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/IPretendingAidl
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -476,6 +416,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/R$Nested
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 2, attributes: 4
+Constant pool:
+{
   public static int[] ARRAY;
     descriptor: [I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
@@ -546,6 +488,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/R
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 2, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -594,6 +538,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 7, attributes: 3
+Constant pool:
+{
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -785,6 +731,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkClassLoadHook
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 3, attributes: 3
+Constant pool:
+{
   public static final java.util.Set<java.lang.Class<?>> sLoadedClasses;
     descriptor: Ljava/util/Set;
     flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
@@ -880,6 +828,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotations
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 5, attributes: 3
+Constant pool:
+{
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -1010,6 +960,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializerDefault
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 0, attributes: 3
+Constant pool:
+{
   public static boolean sInitialized;
     descriptor: Z
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
@@ -1047,6 +999,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializerStub
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 1, attributes: 3
+Constant pool:
+{
   public static boolean sInitialized;
     descriptor: Z
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
@@ -1117,6 +1071,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkEnumComplex
   super_class: #x                         // java/lang/Enum
   interfaces: 0, fields: 6, methods: 7, attributes: 4
+Constant pool:
+{
   public static final com.android.hoststubgen.test.tinyframework.TinyFrameworkEnumComplex RED;
     descriptor: Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkEnumComplex;
     flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
@@ -1400,6 +1356,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkEnumSimple
   super_class: #x                         // java/lang/Enum
   interfaces: 0, fields: 3, methods: 5, attributes: 4
+Constant pool:
+{
   public static final com.android.hoststubgen.test.tinyframework.TinyFrameworkEnumSimple CAT;
     descriptor: Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkEnumSimple;
     flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
@@ -1576,6 +1534,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkExceptionTester
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 3
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -1659,6 +1619,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 15, attributes: 2
+Constant pool:
+{
   public int stub;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -1971,6 +1933,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkLambdas$Nested
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 8, attributes: 6
+Constant pool:
+{
   public final java.util.function.Supplier<java.lang.Integer> mSupplier;
     descriptor: Ljava/util/function/Supplier;
     flags: (0x0011) ACC_PUBLIC, ACC_FINAL
@@ -2201,6 +2165,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkLambdas
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 8, attributes: 6
+Constant pool:
+{
   public final java.util.function.Supplier<java.lang.Integer> mSupplier;
     descriptor: Ljava/util/function/Supplier;
     flags: (0x0011) ACC_PUBLIC, ACC_FINAL
@@ -2432,6 +2398,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkMethodCallReplace$ReplaceTo
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 4, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -2526,6 +2494,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkMethodCallReplace
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 5, attributes: 6
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -2665,6 +2635,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 15, attributes: 3
+Constant pool:
+{
   int value;
     descriptor: I
     flags: (0x0000)
@@ -2998,6 +2970,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 8, attributes: 3
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3173,6 +3147,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 1, methods: 4, attributes: 6
+Constant pool:
+{
   final com.android.hoststubgen.test.tinyframework.TinyFrameworkNestedClasses this$0;
     descriptor: Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
     flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
@@ -3278,6 +3254,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 4, attributes: 6
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3369,6 +3347,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 1, methods: 4, attributes: 6
+Constant pool:
+{
   final com.android.hoststubgen.test.tinyframework.TinyFrameworkNestedClasses this$0;
     descriptor: Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
     flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
@@ -3474,6 +3454,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 4, attributes: 6
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3565,6 +3547,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 2, attributes: 4
+Constant pool:
+{
   public int value;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -3623,6 +3607,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 2, attributes: 4
+Constant pool:
+{
   public int value;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -3694,6 +3680,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 4, attributes: 6
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3786,6 +3774,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$Double$NestedClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 2, attributes: 4
+Constant pool:
+{
   public int value;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -3844,6 +3834,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 3, attributes: 4
+Constant pool:
+{
   public int value;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
@@ -3923,6 +3915,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass
   interfaces: 0, fields: 0, methods: 2, attributes: 4
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3973,6 +3967,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 2, methods: 4, attributes: 5
+Constant pool:
+{
   public final java.util.function.Supplier<java.lang.Integer> mSupplier;
     descriptor: Ljava/util/function/Supplier;
     flags: (0x0011) ACC_PUBLIC, ACC_FINAL
@@ -4121,6 +4117,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 3
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4192,6 +4190,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 3
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4263,6 +4263,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/packagetest/A
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4286,6 +4288,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4309,6 +4313,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/C1
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4332,6 +4338,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/C2
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/subclasstest/C1
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4355,6 +4363,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/C3
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/subclasstest/C2
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4378,6 +4388,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/CA
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4401,6 +4413,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/CB
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4424,6 +4438,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_C1
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/subclasstest/C1
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4447,6 +4463,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_C2
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/subclasstest/C2
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4470,6 +4488,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_C3
   super_class: #x                         // com/android/hoststubgen/test/tinyframework/subclasstest/C3
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4493,6 +4513,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_I1
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4516,6 +4538,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_I1_IA
   super_class: #x                         // java/lang/Object
   interfaces: 2, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4539,6 +4563,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_I2
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4562,6 +4588,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/Class_I3
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4585,6 +4613,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/I1
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4608,6 +4638,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/I2
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4631,6 +4663,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/I3
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4654,6 +4688,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/IA
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4677,6 +4713,8 @@
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/subclasstest/IB
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 1, attributes: 2
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4700,6 +4738,8 @@
   this_class: #x                          // com/supported/UnsupportedClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 3, attributes: 3
+Constant pool:
+{
   private final int mValue;
     descriptor: I
     flags: (0x0012) ACC_PRIVATE, ACC_FINAL
@@ -4779,6 +4819,8 @@
   this_class: #x                          // com/unsupported/UnsupportedClass
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 0, methods: 3, attributes: 3
+Constant pool:
+{
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -4854,6 +4896,8 @@
   this_class: #x                          // rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed
   super_class: #x                         // java/lang/Object
   interfaces: 0, fields: 1, methods: 3, attributes: 3
+Constant pool:
+{
   private final int mValue;
     descriptor: I
     flags: (0x0012) ACC_PRIVATE, ACC_FINAL
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index 700a162..d8e10f8 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -38,7 +38,6 @@
 import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
-import android.app.admin.DevicePolicyManagerInternal;
 import android.app.assist.AssistContent;
 import android.app.assist.AssistStructure;
 import android.app.contextualsearch.CallbackToken;
@@ -67,6 +66,7 @@
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.SystemClock;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.util.Log;
 import android.util.Slog;
@@ -112,8 +112,8 @@
     private final ActivityTaskManagerInternal mAtmInternal;
     private final PackageManagerInternal mPackageManager;
     private final WindowManagerInternal mWmInternal;
-    private final DevicePolicyManagerInternal mDpmInternal;
     private final AudioManager mAudioManager;
+    private final UserManager mUserManager;
     private final Object mLock = new Object();
     private final AssistDataRequester mAssistDataRequester;
 
@@ -179,9 +179,9 @@
                 LocalServices.getService(ActivityTaskManagerInternal.class));
         mPackageManager = LocalServices.getService(PackageManagerInternal.class);
         mAudioManager = context.getSystemService(AudioManager.class);
+        mUserManager = context.getSystemService(UserManager.class);
 
         mWmInternal = Objects.requireNonNull(LocalServices.getService(WindowManagerInternal.class));
-        mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
         mAssistDataRequester = new AssistDataRequester(
                 mContext,
                 IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)),
@@ -308,6 +308,11 @@
         }
     }
 
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.CREATE_USERS,
+            android.Manifest.permission.QUERY_USERS
+    })
     private Intent getContextualSearchIntent(int entrypoint, int userId, CallbackToken mToken) {
         final Intent launchIntent = getResolvedLaunchIntent(userId);
         if (launchIntent == null) {
@@ -338,8 +343,7 @@
                 visiblePackageNames.add(record.getComponentName().getPackageName());
                 activityTokens.add(record.getActivityToken());
             }
-            if (mDpmInternal != null
-                    && mDpmInternal.isUserOrganizationManaged(record.getUserId())) {
+            if (mUserManager.isManagedProfile(record.getUserId())) {
                 isManagedProfileVisible = true;
             }
         }
@@ -507,7 +511,10 @@
                 Intent launchIntent = getContextualSearchIntent(entrypoint, callingUserId, mToken);
                 if (launchIntent != null) {
                     int result = invokeContextualSearchIntent(launchIntent, callingUserId);
-                    if (DEBUG) Log.d(TAG, "Launch result: " + result);
+                    if (DEBUG) {
+                        Log.d(TAG, "Launch intent: " + launchIntent);
+                        Log.d(TAG, "Launch result: " + result);
+                    }
                 }
             });
         }
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 4976a63..8eda176 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -18,7 +18,6 @@
 
 import static android.app.Flags.enableCurrentModeTypeBinderCache;
 import static android.app.Flags.enableNightModeBinderCache;
-import static android.app.Flags.modesApi;
 import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
 import static android.app.UiModeManager.DEFAULT_PRIORITY;
 import static android.app.UiModeManager.FORCE_INVERT_TYPE_DARK;
@@ -2208,14 +2207,12 @@
             appliedOverrides = true;
         }
 
-        if (modesApi()) {
-            // Computes final night mode values based on Attention Mode.
-            mComputedNightMode = switch (mAttentionModeThemeOverlay) {
-                case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true;
-                case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false;
-                default -> newComputedValue; // case OFF
-            };
-        }
+        // Computes final night mode values based on Attention Mode.
+        mComputedNightMode = switch (mAttentionModeThemeOverlay) {
+            case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true;
+            case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false;
+            default -> newComputedValue; // case OFF
+        };
 
         if (appliedOverrides) {
             return;
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 961022b..517279b 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -54,15 +54,21 @@
 import com.android.internal.app.ProcessMap;
 import com.android.internal.os.Clock;
 import com.android.internal.os.MonotonicClock;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.ServiceThread;
 import com.android.server.SystemServiceManager;
 import com.android.server.wm.WindowProcessController;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -1006,6 +1012,12 @@
             throws IOException, WireTypeMismatchException, ClassNotFoundException {
         long token = proto.start(fieldId);
         String pkgName = "";
+
+        // Create objects for reuse.
+        ByteArrayInputStream byteArrayInputStream = null;
+        ObjectInputStream objectInputStream = null;
+        TypedXmlPullParser typedXmlPullParser = null;
+
         for (int next = proto.nextField();
                 next != ProtoInputStream.NO_MORE_FIELDS;
                 next = proto.nextField()) {
@@ -1017,7 +1029,7 @@
                     AppStartInfoContainer container =
                             new AppStartInfoContainer(mAppStartInfoHistoryListSize);
                     int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS,
-                            pkgName);
+                            pkgName, byteArrayInputStream, objectInputStream, typedXmlPullParser);
 
                     // If the isolated process flag is enabled and the uid is that of an isolated
                     // process, then break early so that the container will not be added to mData.
@@ -1052,6 +1064,12 @@
             out = af.startWrite();
             ProtoOutputStream proto = new ProtoOutputStream(out);
             proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now);
+
+            // Create objects for reuse.
+            ByteArrayOutputStream byteArrayOutputStream = null;
+            ObjectOutputStream objectOutputStream = null;
+            TypedXmlSerializer typedXmlSerializer = null;
+
             synchronized (mLock) {
                 succeeded = forEachPackageLocked(
                         (packageName, records) -> {
@@ -1060,8 +1078,9 @@
                             int uidArraySize = records.size();
                             for (int j = 0; j < uidArraySize; j++) {
                                 try {
-                                    records.valueAt(j)
-                                            .writeToProto(proto, AppsStartInfoProto.Package.USERS);
+                                    records.valueAt(j).writeToProto(proto,
+                                            AppsStartInfoProto.Package.USERS, byteArrayOutputStream,
+                                            objectOutputStream, typedXmlSerializer);
                                 } catch (IOException e) {
                                     Slog.w(TAG, "Unable to write app start info into persistent"
                                             + "storage: " + e);
@@ -1414,19 +1433,23 @@
         }
 
         @GuardedBy("mLock")
-        void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+        void writeToProto(ProtoOutputStream proto, long fieldId,
+                ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream,
+                TypedXmlSerializer typedXmlSerializer) throws IOException {
             long token = proto.start(fieldId);
             proto.write(AppsStartInfoProto.Package.User.UID, mUid);
             int size = mInfos.size();
             for (int i = 0; i < size; i++) {
-                mInfos.get(i)
-                        .writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+                mInfos.get(i).writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO,
+                        byteArrayOutputStream, objectOutputStream, typedXmlSerializer);
             }
             proto.write(AppsStartInfoProto.Package.User.MONITORING_ENABLED, mMonitoringModeEnabled);
             proto.end(token);
         }
 
-        int readFromProto(ProtoInputStream proto, long fieldId, String packageName)
+        int readFromProto(ProtoInputStream proto, long fieldId, String packageName,
+                ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream,
+                TypedXmlPullParser typedXmlPullParser)
                 throws IOException, WireTypeMismatchException, ClassNotFoundException {
             long token = proto.start(fieldId);
             for (int next = proto.nextField();
@@ -1440,7 +1463,8 @@
                         // Create record with monotonic time 0 in case the persisted record does not
                         // have a create time.
                         ApplicationStartInfo info = new ApplicationStartInfo(0);
-                        info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+                        info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO,
+                                byteArrayInputStream, objectInputStream, typedXmlPullParser);
                         info.setPackageName(packageName);
                         mInfos.add(info);
                         break;
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index 30c2a82..604cb30 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -418,7 +418,9 @@
                         evictedEvents.addAll(mCache);
                         mCache.clear();
                     }
-                    mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
+                    Message msg = mSqliteWriteHandler.obtainMessage(
+                            WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
+                    mSqliteWriteHandler.sendMessage(msg);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 6d6e1fb..ef80d59 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -18,6 +18,7 @@
 import static android.media.audio.Flags.scoManagedByAudio;
 
 import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
 import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET;
 import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER;
 import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO;
@@ -290,8 +291,8 @@
     }
 
     @GuardedBy("mDeviceStateLock")
-    /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
-        mBtHelper.onSetBtScoActiveDevice(btDevice);
+    /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) {
+        mBtHelper.onSetBtScoActiveDevice(btDevice, deviceSwitch);
     }
 
     /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
@@ -941,6 +942,7 @@
         final @NonNull String mEventSource;
         final int mAudioSystemDevice;
         final int mMusicDevice;
+        final boolean mIsDeviceSwitch;
 
         BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state,
                     int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) {
@@ -953,6 +955,8 @@
             mEventSource = d.mEventSource;
             mAudioSystemDevice = audioDevice;
             mMusicDevice = AudioSystem.DEVICE_NONE;
+            mIsDeviceSwitch = optimizeBtDeviceSwitch()
+                    && d.mNewDevice != null && d.mPreviousDevice != null;
         }
 
         // constructor used by AudioDeviceBroker to search similar message
@@ -966,6 +970,7 @@
             mSupprNoisy = false;
             mVolume = -1;
             mIsLeOutput = false;
+            mIsDeviceSwitch = false;
         }
 
         // constructor used by AudioDeviceInventory when config change failed
@@ -980,6 +985,7 @@
             mSupprNoisy = false;
             mVolume = -1;
             mIsLeOutput = false;
+            mIsDeviceSwitch = false;
         }
 
         BtDeviceInfo(@NonNull BtDeviceInfo src, int state) {
@@ -992,6 +998,7 @@
             mEventSource = src.mEventSource;
             mAudioSystemDevice = src.mAudioSystemDevice;
             mMusicDevice = src.mMusicDevice;
+            mIsDeviceSwitch = false;
         }
 
         // redefine equality op so we can match messages intended for this device
@@ -1026,7 +1033,8 @@
                             + " isLeOutput=" + mIsLeOutput
                             + " eventSource=" + mEventSource
                             + " audioSystemDevice=" + mAudioSystemDevice
-                            + " musicDevice=" + mMusicDevice;
+                            + " musicDevice=" + mMusicDevice
+                            + " isDeviceSwitch=" + mIsDeviceSwitch;
         }
     }
 
@@ -1196,6 +1204,8 @@
             AudioSystem.setParameters("A2dpSuspended=true");
             AudioSystem.setParameters("LeAudioSuspended=true");
             AudioSystem.setParameters("BT_SCO=on");
+            mBluetoothA2dpSuspendedApplied = true;
+            mBluetoothLeSuspendedApplied = true;
         } else {
             AudioSystem.setParameters("BT_SCO=off");
             if (mBluetoothA2dpSuspendedApplied) {
@@ -1680,10 +1690,11 @@
     }
 
     /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
-                                boolean connect, @Nullable BluetoothDevice btDevice) {
+                                boolean connect, @Nullable BluetoothDevice btDevice,
+                                boolean deviceSwitch) {
         synchronized (mDeviceStateLock) {
             return mDeviceInventory.handleDeviceConnection(
-                    attributes, connect, false /*for test*/, btDevice);
+                    attributes, connect, false /*for test*/, btDevice, deviceSwitch);
         }
     }
 
@@ -1776,6 +1787,18 @@
 
         pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio);
 
+        pw.println("\n" + prefix + "Bluetooth SCO on"
+                + ", requested: " + mBluetoothScoOn
+                + ", applied: " + mBluetoothScoOnApplied);
+        pw.println("\n" + prefix +  "Bluetooth A2DP suspended"
+                + ", requested ext: " + mBluetoothA2dpSuspendedExt
+                + ", requested int: " + mBluetoothA2dpSuspendedInt
+                + ", applied " + mBluetoothA2dpSuspendedApplied);
+        pw.println("\n" + prefix +  "Bluetooth LE Audio suspended"
+                + ", requested ext: " + mBluetoothLeSuspendedExt
+                + ", requested int: " + mBluetoothLeSuspendedInt
+                + ", applied " + mBluetoothLeSuspendedApplied);
+
         mBtHelper.dump(pw, prefix);
     }
 
@@ -1930,10 +1953,12 @@
                                                 || btInfo.mIsLeOutput)
                                             ? mAudioService.getBluetoothContextualVolumeStream()
                                             : AudioSystem.STREAM_DEFAULT);
-                                if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
+                                if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                         || btInfo.mProfile == BluetoothProfile.HEARING_AID
                                         || (mScoManagedByAudio
-                                            && btInfo.mProfile == BluetoothProfile.HEADSET)) {
+                                            && btInfo.mProfile == BluetoothProfile.HEADSET))
+                                        && (btInfo.mState == BluetoothProfile.STATE_CONNECTED
+                                            || !btInfo.mIsDeviceSwitch)) {
                                     onUpdateCommunicationRouteClient(
                                             bluetoothScoRequestOwnerAttributionSource(),
                                             "setBluetoothActiveDevice");
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ef10793..ae91934 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -799,7 +799,7 @@
                         di.mDeviceAddress,
                         di.mDeviceName),
                         AudioSystem.DEVICE_STATE_AVAILABLE,
-                        di.mDeviceCodecFormat);
+                        di.mDeviceCodecFormat, false /*deviceSwitch*/);
                 if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
                     failedReconnectionDeviceList.add(di);
                 }
@@ -811,7 +811,7 @@
                             EventLogger.Event.ALOGE, TAG);
                     mConnectedDevices.remove(di.getKey(), di);
                     if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) {
-                        mDeviceBroker.onSetBtScoActiveDevice(null);
+                        mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/);
                     }
                 }
             }
@@ -851,7 +851,8 @@
             Log.d(TAG, "onSetBtActiveDevice"
                     + " btDevice=" + btInfo.mDevice
                     + " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile)
-                    + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState));
+                    + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState)
+                    + " isDeviceSwitch=" + btInfo.mIsDeviceSwitch);
         }
         String address = btInfo.mDevice.getAddress();
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
@@ -897,7 +898,8 @@
                     break;
                 case BluetoothProfile.A2DP:
                     if (switchToUnavailable) {
-                        makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+                        makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat,
+                                                     btInfo.mIsDeviceSwitch);
                     } else if (switchToAvailable) {
                         // device is not already connected
                         if (btInfo.mVolume != -1) {
@@ -911,7 +913,7 @@
                     break;
                 case BluetoothProfile.HEARING_AID:
                     if (switchToUnavailable) {
-                        makeHearingAidDeviceUnavailable(address);
+                        makeHearingAidDeviceUnavailable(address, btInfo.mIsDeviceSwitch);
                     } else if (switchToAvailable) {
                         makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
                                 streamType, "onSetBtActiveDevice");
@@ -921,7 +923,8 @@
                 case BluetoothProfile.LE_AUDIO_BROADCAST:
                     if (switchToUnavailable) {
                         makeLeAudioDeviceUnavailableNow(address,
-                                btInfo.mAudioSystemDevice, di.mDeviceCodecFormat);
+                                btInfo.mAudioSystemDevice, di.mDeviceCodecFormat,
+                                btInfo.mIsDeviceSwitch);
                     } else if (switchToAvailable) {
                         makeLeAudioDeviceAvailable(
                                 btInfo, streamType, codec, "onSetBtActiveDevice");
@@ -930,9 +933,10 @@
                 case BluetoothProfile.HEADSET:
                     if (mDeviceBroker.isScoManagedByAudio()) {
                         if (switchToUnavailable) {
-                            mDeviceBroker.onSetBtScoActiveDevice(null);
+                            mDeviceBroker.onSetBtScoActiveDevice(null, btInfo.mIsDeviceSwitch);
                         } else if (switchToAvailable) {
-                            mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice);
+                            mDeviceBroker.onSetBtScoActiveDevice(
+                                    btInfo.mDevice, false /*deviceSwitch*/);
                         }
                     }
                     break;
@@ -1053,19 +1057,19 @@
 
     /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
         synchronized (mDevicesLock) {
-            makeA2dpDeviceUnavailableNow(address, a2dpCodec);
+            makeA2dpDeviceUnavailableNow(address, a2dpCodec, false /*deviceSwitch*/);
         }
     }
 
     /*package*/ void onMakeLeAudioDeviceUnavailableNow(String address, int device, int codec) {
         synchronized (mDevicesLock) {
-            makeLeAudioDeviceUnavailableNow(address, device, codec);
+            makeLeAudioDeviceUnavailableNow(address, device, codec, false /*deviceSwitch*/);
         }
     }
 
     /*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) {
         synchronized (mDevicesLock) {
-            makeHearingAidDeviceUnavailable(address);
+            makeHearingAidDeviceUnavailable(address, false /*deviceSwitch*/);
         }
     }
 
@@ -1180,7 +1184,8 @@
             }
 
             if (!handleDeviceConnection(wdcs.mAttributes,
-                    wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) {
+                    wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest,
+                    null, false /*deviceSwitch*/)) {
                 // change of connection state failed, bailout
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
                         .record();
@@ -1788,14 +1793,15 @@
      */
     /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
                                                boolean connect, boolean isForTesting,
-                                               @Nullable BluetoothDevice btDevice) {
+                                               @Nullable BluetoothDevice btDevice,
+                                               boolean deviceSwitch) {
         int device = attributes.getInternalType();
         String address = attributes.getAddress();
         String deviceName = attributes.getName();
         if (AudioService.DEBUG_DEVICES) {
             Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:"
                     + Integer.toHexString(device) + " address:" + address
-                    + " name:" + deviceName + ")");
+                    + " name:" + deviceName + ", deviceSwitch: " + deviceSwitch + ")");
         }
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection")
                 .set(MediaMetrics.Property.ADDRESS, address)
@@ -1829,7 +1835,8 @@
                     res = AudioSystem.AUDIO_STATUS_OK;
                 } else {
                     res = mAudioSystem.setDeviceConnectionState(attributes,
-                            AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
+                            AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT,
+                            false /*deviceSwitch*/);
                 }
                 if (res != AudioSystem.AUDIO_STATUS_OK) {
                     final String reason = "not connecting device 0x" + Integer.toHexString(device)
@@ -1856,7 +1863,8 @@
                 status = true;
             } else if (!connect && isConnected) {
                 mAudioSystem.setDeviceConnectionState(attributes,
-                        AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
+                        AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT,
+                        deviceSwitch);
                 // always remove even if disconnection failed
                 mConnectedDevices.remove(deviceKey);
                 mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes);
@@ -2030,7 +2038,7 @@
             }
         }
         if (disconnect) {
-            mDeviceBroker.onSetBtScoActiveDevice(null);
+            mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/);
         }
     }
 
@@ -2068,7 +2076,8 @@
                         || info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST)
                         && info.mIsLeOutput)
                         || info.mProfile == BluetoothProfile.HEARING_AID
-                        || info.mProfile == BluetoothProfile.A2DP)) {
+                        || info.mProfile == BluetoothProfile.A2DP)
+                    && !info.mIsDeviceSwitch) {
                 @AudioService.ConnectionState int asState =
                         (info.mState == BluetoothProfile.STATE_CONNECTED)
                                 ? AudioService.CONNECTION_STATE_CONNECTED
@@ -2124,7 +2133,7 @@
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
                 AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name);
         final int res = mAudioSystem.setDeviceConnectionState(ada,
-                AudioSystem.DEVICE_STATE_AVAILABLE, codec);
+                AudioSystem.DEVICE_STATE_AVAILABLE, codec, false);
 
         // TODO: log in MediaMetrics once distinction between connection failure and
         // double connection is made.
@@ -2362,7 +2371,7 @@
     }
 
     @GuardedBy("mDevicesLock")
-    private void makeA2dpDeviceUnavailableNow(String address, int codec) {
+    private void makeA2dpDeviceUnavailableNow(String address, int codec, boolean deviceSwitch) {
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
                 .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(codec))
                 .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow");
@@ -2393,7 +2402,7 @@
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
                 AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
         final int res = mAudioSystem.setDeviceConnectionState(ada,
-                AudioSystem.DEVICE_STATE_UNAVAILABLE, codec);
+                AudioSystem.DEVICE_STATE_UNAVAILABLE, codec, deviceSwitch);
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
@@ -2404,7 +2413,8 @@
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                     "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
-                            + " made unavailable")).printSlog(EventLogger.Event.ALOGI, TAG));
+                            + " made unavailable, deviceSwitch" + deviceSwitch))
+                    .printSlog(EventLogger.Event.ALOGI, TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
 
@@ -2440,7 +2450,7 @@
         final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
                 AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
                 AudioSystem.DEVICE_STATE_AVAILABLE,
-                AudioSystem.AUDIO_FORMAT_DEFAULT);
+                AudioSystem.AUDIO_FORMAT_DEFAULT, false);
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "APM failed to make available A2DP source device addr="
@@ -2465,7 +2475,7 @@
                 AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
-                AudioSystem.AUDIO_FORMAT_DEFAULT);
+                AudioSystem.AUDIO_FORMAT_DEFAULT, false);
         // always remove regardless of the result
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
@@ -2485,7 +2495,7 @@
                 DEVICE_OUT_HEARING_AID, address, name);
         final int res = mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
-                AudioSystem.AUDIO_FORMAT_DEFAULT);
+                AudioSystem.AUDIO_FORMAT_DEFAULT, false);
         if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueueAndSlog(
                     "APM failed to make available HearingAid addr=" + address
@@ -2515,12 +2525,12 @@
     }
 
     @GuardedBy("mDevicesLock")
-    private void makeHearingAidDeviceUnavailable(String address) {
+    private void makeHearingAidDeviceUnavailable(String address, boolean deviceSwitch) {
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
                 DEVICE_OUT_HEARING_AID, address);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
-                AudioSystem.AUDIO_FORMAT_DEFAULT);
+                AudioSystem.AUDIO_FORMAT_DEFAULT, deviceSwitch);
         // always remove regardless of return code
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
@@ -2622,7 +2632,7 @@
 
             AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name);
             final int res = mAudioSystem.setDeviceConnectionState(ada,
-                    AudioSystem.DEVICE_STATE_AVAILABLE, codec);
+                    AudioSystem.DEVICE_STATE_AVAILABLE, codec, false /*deviceSwitch*/);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
                 AudioService.sDeviceLogger.enqueueAndSlog(
                         "APM failed to make available LE Audio device addr=" + address
@@ -2669,13 +2679,13 @@
 
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceUnavailableNow(String address, int device,
-            @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) {
+            @AudioSystem.AudioFormatNativeEnumForBtCodec int codec,  boolean deviceSwitch) {
         AudioDeviceAttributes ada = null;
         if (device != AudioSystem.DEVICE_NONE) {
             ada = new AudioDeviceAttributes(device, address);
             final int res = mAudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_UNAVAILABLE,
-                    codec);
+                    codec, deviceSwitch);
 
             if (res != AudioSystem.AUDIO_STATUS_OK) {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
@@ -2685,7 +2695,8 @@
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
-                                + " made unavailable").printSlog(EventLogger.Event.ALOGI, TAG));
+                            + " made unavailable, deviceSwitch" + deviceSwitch)
+                        .printSlog(EventLogger.Event.ALOGI, TAG));
             }
             mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 86871ea..813e661 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.deferWearPermissionUpdates;
 import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
 import static com.android.media.audio.Flags.replaceStreamBtSco;
 import static com.android.media.audio.Flags.ringMyCar;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
@@ -585,6 +586,9 @@
     // protects mRingerMode
     private final Object mSettingsLock = new Object();
 
+    // protects VolumeStreamState / VolumeGroupState operations
+    private final Object mVolumeStateLock = new Object();
+
    /** Maximum volume index values for audio streams */
     protected static int[] MAX_STREAM_VOLUME = new int[] {
         5,  // STREAM_VOICE_CALL
@@ -1611,7 +1615,7 @@
 
     private void initVolumeStreamStates() {
         int numStreamTypes = AudioSystem.getNumStreamTypes();
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
                 final VolumeStreamState streamState = getVssForStream(streamType);
                 if (streamState == null) {
@@ -2419,7 +2423,7 @@
 
     private void checkAllAliasStreamVolumes() {
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 int numStreamTypes = AudioSystem.getNumStreamTypes();
                 for (int streamType = 0; streamType < numStreamTypes; streamType++) {
                     int streamAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
@@ -2501,7 +2505,7 @@
     private void onUpdateVolumeStatesForAudioDevice(int device, String caller) {
         final int numStreamTypes = AudioSystem.getNumStreamTypes();
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 for (int streamType = 0; streamType < numStreamTypes; streamType++) {
                     updateVolumeStates(device, streamType, caller);
                 }
@@ -2770,7 +2774,7 @@
             updateDefaultVolumes();
 
             synchronized (mSettingsLock) {
-                synchronized (VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     getVssForStreamOrDefault(AudioSystem.STREAM_DTMF)
                             .setAllIndexes(getVssForStreamOrDefault(dtmfStreamAlias), caller);
                     getVssForStreamOrDefault(AudioSystem.STREAM_ACCESSIBILITY).setSettingName(
@@ -3232,8 +3236,10 @@
         // Each stream will read its own persisted settings
 
         // Broadcast the sticky intents
-        broadcastRingerMode(AudioManager.RINGER_MODE_CHANGED_ACTION, mRingerModeExternal);
-        broadcastRingerMode(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, mRingerMode);
+        synchronized (mSettingsLock) {
+            broadcastRingerMode(AudioManager.RINGER_MODE_CHANGED_ACTION, mRingerModeExternal);
+            broadcastRingerMode(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, mRingerMode);
+        }
 
         // Broadcast vibrate settings
         broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER);
@@ -4235,7 +4241,7 @@
     private void muteAliasStreams(int streamAlias, boolean state) {
         // Locking mSettingsLock to avoid inversion when calling doMute -> updateVolumeGroupIndex
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 List<Integer> streamsToMute = new ArrayList<>();
                 for (int streamIdx = 0; streamIdx < mStreamStates.size(); streamIdx++) {
                     final VolumeStreamState vss = mStreamStates.valueAt(streamIdx);
@@ -4279,7 +4285,7 @@
         // Locking mSettingsLock to avoid inversion when calling vss.mute -> vss.doMute ->
         // vss.updateVolumeGroupIndex
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 final VolumeStreamState streamState = getVssForStreamOrDefault(streamAlias);
                 // if unmuting causes a change, it was muted
                 wasMuted = streamState.mute(false, "onUnmuteStreamOnSingleVolDevice");
@@ -4455,7 +4461,7 @@
     /** @see AudioManager#getVolumeGroupVolumeIndex(int) */
     public int getVolumeGroupVolumeIndex(int groupId) {
         super.getVolumeGroupVolumeIndex_enforcePermission();
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
                 Log.e(TAG, "No volume group for id " + groupId);
                 return 0;
@@ -4472,7 +4478,7 @@
             MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING })
     public int getVolumeGroupMaxVolumeIndex(int groupId) {
         super.getVolumeGroupMaxVolumeIndex_enforcePermission();
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
                 Log.e(TAG, "No volume group for id " + groupId);
                 return 0;
@@ -4487,7 +4493,7 @@
             MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING })
     public int getVolumeGroupMinVolumeIndex(int groupId) {
         super.getVolumeGroupMinVolumeIndex_enforcePermission();
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
                 Log.e(TAG, "No volume group for id " + groupId);
                 return 0;
@@ -4632,7 +4638,7 @@
     @android.annotation.EnforcePermission(QUERY_AUDIO_STATE)
     public int getLastAudibleVolumeForVolumeGroup(int groupId) {
         super.getLastAudibleVolumeForVolumeGroup_enforcePermission();
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
                 Log.e(TAG, ": no volume group found for id " + groupId);
                 return 0;
@@ -4644,7 +4650,7 @@
 
     /** @see AudioManager#isVolumeGroupMuted(int) */
     public boolean isVolumeGroupMuted(int groupId) {
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
                 Log.e(TAG, ": no volume group found for id " + groupId);
                 return false;
@@ -4990,6 +4996,8 @@
                 + cacheGetStreamMinMaxVolume());
         pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:"
                 + cacheGetStreamVolume());
+        pw.println("\tcom.android.media.audio.optimizeBtDeviceSwitch:"
+                + optimizeBtDeviceSwitch());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -5470,7 +5478,7 @@
         }
         streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamMute");
 
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             ensureValidStreamType(streamType);
             return getVssForStreamOrDefault(streamType).mIsMuted;
         }
@@ -5651,7 +5659,7 @@
     }
 
     private int getStreamVolume(int streamType, int device) {
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             final VolumeStreamState vss = getVssForStreamOrDefault(streamType);
             int index = vss.getIndex(device);
 
@@ -5695,7 +5703,7 @@
 
         vib.setMinVolumeIndex((vss.mIndexMin + 5) / 10);
         vib.setMaxVolumeIndex((vss.mIndexMax + 5) / 10);
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             final int index;
             if (isFixedVolumeDevice(ada.getInternalType())) {
                 index = (vss.mIndexMax + 5) / 10;
@@ -6263,7 +6271,7 @@
                 // ring and notifications volume should never be 0 when not silenced
                 if (sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_RING
                         || sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_NOTIFICATION) {
-                    synchronized (VolumeStreamState.class) {
+                    synchronized (mVolumeStateLock) {
                         for (int i = 0; i < vss.mIndexMap.size(); i++) {
                             int device = vss.mIndexMap.keyAt(i);
                             int value = vss.mIndexMap.valueAt(i);
@@ -7023,7 +7031,7 @@
             }
 
             streamState.readSettings();
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // unmute stream that was muted but is not affect by mute anymore
                 if (streamState.mIsMuted && ((!isStreamAffectedByMute(streamType) &&
                         !isStreamMutedByRingerOrZenMode(streamType)) || mUseFixedVolume)) {
@@ -8056,14 +8064,14 @@
     public Set<Integer> getDeviceSetForStream(int stream) {
         stream = replaceBtScoStreamWithVoiceCall(stream, "getDeviceSetForStream");
         ensureValidStreamType(stream);
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             return getVssForStreamOrDefault(stream).observeDevicesForStream_syncVSS(true);
         }
     }
 
     private void onObserveDevicesForAllStreams(int skipStream) {
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 for (int stream = 0; stream < mStreamStates.size(); stream++) {
                     final VolumeStreamState vss = mStreamStates.valueAt(stream);
                     if (vss != null && vss.getStreamType() != skipStream) {
@@ -8645,7 +8653,7 @@
 
     private void readVolumeGroupsSettings(boolean userSwitch) {
         synchronized (mSettingsLock) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 if (DEBUG_VOL) {
                     Log.d(TAG, "readVolumeGroupsSettings userSwitch=" + userSwitch);
                 }
@@ -8703,7 +8711,7 @@
 
     // NOTE: Locking order for synchronized objects related to volume management:
     //  1     mSettingsLock
-    //  2       VolumeStreamState.class
+    //  2       mVolumeStateLock
     private class VolumeGroupState {
         private final AudioVolumeGroup mAudioVolumeGroup;
         private final SparseIntArray mIndexMap = new SparseIntArray(8);
@@ -8797,7 +8805,7 @@
          * Mute/unmute the volume group
          * @param muted the new mute state
          */
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         public boolean mute(boolean muted) {
             if (!isMutable()) {
                 // Non mutable volume group
@@ -8821,7 +8829,7 @@
 
         public void adjustVolume(int direction, int flags) {
             synchronized (mSettingsLock) {
-                synchronized (AudioService.VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     int device = getDeviceForVolume();
                     int previousIndex = getIndex(device);
                     if (isMuteAdjust(direction) && !isMutable()) {
@@ -8875,14 +8883,14 @@
         }
 
         public int getVolumeIndex() {
-            synchronized (AudioService.VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 return getIndex(getDeviceForVolume());
             }
         }
 
         public void setVolumeIndex(int index, int flags) {
             synchronized (mSettingsLock) {
-                synchronized (AudioService.VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     if (mUseFixedVolume) {
                         return;
                     }
@@ -8891,7 +8899,7 @@
             }
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         private void setVolumeIndex(int index, int device, int flags) {
             // Update cache & persist (muted by volume 0 shall be persisted)
             updateVolumeIndex(index, device);
@@ -8904,7 +8912,7 @@
             }
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         public void updateVolumeIndex(int index, int device) {
             // Filter persistency if already exist and the index has not changed
             if (mIndexMap.indexOfKey(device) < 0 || mIndexMap.get(device) != index) {
@@ -8922,7 +8930,7 @@
             }
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         private void setVolumeIndexInt(int index, int device, int flags) {
             // Reflect mute state of corresponding stream by forcing index to 0 if muted
             // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
@@ -8955,14 +8963,14 @@
             mAudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, muted, device);
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         private int getIndex(int device) {
             int index = mIndexMap.get(device, -1);
             // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
             return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         private boolean hasIndexForDevice(int device) {
             return (mIndexMap.get(device, -1) != -1);
         }
@@ -8989,7 +8997,7 @@
 
         public void applyAllVolumes(boolean userSwitch) {
             String caller = "from vgs";
-            synchronized (AudioService.VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // apply device specific volumes first
                 for (int i = 0; i < mIndexMap.size(); i++) {
                     int device = mIndexMap.keyAt(i);
@@ -9092,25 +9100,27 @@
             if (mUseFixedVolume || mHasValidStreamType) {
                 return;
             }
-            if (DEBUG_VOL) {
-                Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group "
-                        + mAudioVolumeGroup.name()
-                        + ", device " + AudioSystem.getOutputDeviceName(device)
-                        + " and User=" + getCurrentUserId()
-                        + " mSettingName: " + mSettingName);
-            }
+            synchronized (mVolumeStateLock) {
+                if (DEBUG_VOL) {
+                    Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device)
+                            + " for group " + mAudioVolumeGroup.name()
+                            + ", device " + AudioSystem.getOutputDeviceName(device)
+                            + " and User=" + getCurrentUserId()
+                            + " mSettingName: " + mSettingName);
+                }
 
-            boolean success = mSettings.putSystemIntForUser(mContentResolver,
-                    getSettingNameForDevice(device),
-                    getIndex(device),
-                    getVolumePersistenceUserId());
-            if (!success) {
-                Log.e(TAG, "persistVolumeGroup failed for group " +  mAudioVolumeGroup.name());
+                boolean success = mSettings.putSystemIntForUser(mContentResolver,
+                        getSettingNameForDevice(device),
+                        getIndex(device),
+                        getVolumePersistenceUserId());
+                if (!success) {
+                    Log.e(TAG, "persistVolumeGroup failed for group " +  mAudioVolumeGroup.name());
+                }
             }
         }
 
         public void readSettings() {
-            synchronized (AudioService.VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // force maximum volume on all streams if fixed volume property is set
                 if (mUseFixedVolume) {
                     mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
@@ -9144,7 +9154,7 @@
             }
         }
 
-        @GuardedBy("AudioService.VolumeStreamState.class")
+        @GuardedBy("AudioService.this.mVolumeStateLock")
         private int getValidIndex(int index) {
             if (index < mIndexMin) {
                 return mIndexMin;
@@ -9218,7 +9228,7 @@
     //  1 mScoclient OR mSafeMediaVolumeState
     //  2   mSetModeLock
     //  3     mSettingsLock
-    //  4       VolumeStreamState.class
+    //  4       mVolumeStateLock
     /*package*/ class VolumeStreamState {
         private final int mStreamType;
         private VolumeGroupState mVolumeGroupState = null;
@@ -9427,7 +9437,7 @@
          *
          * This is a reference to the local list, do not modify.
          */
-        @GuardedBy("VolumeStreamState.class")
+        @GuardedBy("mVolumeStateLock")
         @NonNull
         public Set<Integer> observeDevicesForStream_syncVSS(
                 boolean checkOthers) {
@@ -9495,7 +9505,7 @@
 
         public void readSettings() {
             synchronized (mSettingsLock) {
-                synchronized (VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     // force maximum volume on all streams if fixed volume property is set
                     if (mUseFixedVolume) {
                         mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
@@ -9515,7 +9525,7 @@
                     }
                 }
             }
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
 
                     // retrieve current volume for device
@@ -9548,7 +9558,7 @@
          * will send the non-zero index together with muted state. Otherwise, index 0  will be sent
          * to native for signalising a muted stream.
          **/
-        @GuardedBy("VolumeStreamState.class")
+        @GuardedBy("mVolumeStateLock")
         private void setStreamVolumeIndex(int index, int device) {
             // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
             // This allows RX path muting by the audio HAL only when explicitly muted but not when
@@ -9570,8 +9580,8 @@
             mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, muted, device);
         }
 
-        // must be called while synchronized VolumeStreamState.class
-        @GuardedBy("VolumeStreamState.class")
+        // must be called while synchronized mVolumeStateLock
+        @GuardedBy("mVolumeStateLock")
         /*package*/ void applyDeviceVolume_syncVSS(int device) {
             int index;
             if (isFullyMuted() && !ringMyCar()) {
@@ -9597,7 +9607,7 @@
         }
 
         public void applyAllVolumes() {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // apply device specific volumes first
                 int index;
                 boolean isAbsoluteVolume = false;
@@ -9659,7 +9669,7 @@
             final boolean isCurrentDevice;
             final StringBuilder aliasStreamIndexes = new StringBuilder();
             synchronized (mSettingsLock) {
-                synchronized (VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     oldIndex = getIndex(device);
                     index = getValidIndex(index, hasModifyAudioSettings);
                     // for STREAM_SYSTEM_ENFORCED, do not sync aliased streams on the enforced index
@@ -9777,7 +9787,7 @@
         }
 
         public int getIndex(int device) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 int index = mIndexMap.get(device, -1);
                 if (index == -1) {
                     // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
@@ -9788,7 +9798,7 @@
         }
 
         public @NonNull VolumeInfo getVolumeInfo(int device) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 int index = mIndexMap.get(device, -1);
                 if (index == -1) {
                     // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
@@ -9805,7 +9815,7 @@
         }
 
         public boolean hasIndexForDevice(int device) {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 return (mIndexMap.get(device, -1) != -1);
             }
         }
@@ -9837,8 +9847,8 @@
          * @param srcStream
          * @param caller
          */
-        // must be sync'd on mSettingsLock before VolumeStreamState.class
-        @GuardedBy("VolumeStreamState.class")
+        // must be sync'd on mSettingsLock before mVolumeStateLock
+        @GuardedBy("mVolumeStateLock")
         public void setAllIndexes(VolumeStreamState srcStream, String caller) {
             if (srcStream == null || mStreamType == srcStream.mStreamType) {
                 return;
@@ -9862,8 +9872,8 @@
             }
         }
 
-        // must be sync'd on mSettingsLock before VolumeStreamState.class
-        @GuardedBy("VolumeStreamState.class")
+        // must be sync'd on mSettingsLock before mVolumeStateLock
+        @GuardedBy("mVolumeStateLock")
         public void setAllIndexesToMax() {
             for (int i = 0; i < mIndexMap.size(); i++) {
                 mIndexMap.put(mIndexMap.keyAt(i), mIndexMax);
@@ -9876,7 +9886,7 @@
             // vss.setIndex which grabs this lock after VSS.class. Locking order needs to be
             // preserved
             synchronized (mSettingsLock) {
-                synchronized (VolumeStreamState.class) {
+                synchronized (mVolumeStateLock) {
                     if (mVolumeGroupState != null) {
                         int groupIndex = (getIndex(device) + 5) / 10;
                         if (DEBUG_VOL) {
@@ -9908,7 +9918,7 @@
          */
         public boolean mute(boolean state, String source) {
             boolean changed = false;
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 changed = mute(state, true, source);
             }
             if (changed) {
@@ -9924,7 +9934,7 @@
          */
         public boolean muteInternally(boolean state) {
             boolean changed = false;
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 if (state != mIsMutedInternally) {
                     changed = true;
                     mIsMutedInternally = state;
@@ -9939,7 +9949,7 @@
             return changed;
         }
 
-        @GuardedBy("VolumeStreamState.class")
+        @GuardedBy("mVolumeStateLock")
         public boolean isFullyMuted() {
             return mIsMuted || mIsMutedInternally;
         }
@@ -9960,7 +9970,7 @@
          */
         public boolean mute(boolean state, boolean apply, String src) {
             boolean changed;
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 changed = state != mIsMuted;
                 if (changed) {
                     sMuteLogger.enqueue(
@@ -9994,7 +10004,7 @@
         }
 
         public void doMute() {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // If associated to volume group, update group cache
                 updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */true);
 
@@ -10015,7 +10025,7 @@
         }
 
         public void checkFixedVolumeDevices() {
-            synchronized (VolumeStreamState.class) {
+            synchronized (mVolumeStateLock) {
                 // ignore settings for fixed volume devices: volume should always be at max or 0
                 if (sStreamVolumeAlias.get(mStreamType) == AudioSystem.STREAM_MUSIC) {
                     for (int i = 0; i < mIndexMap.size(); i++) {
@@ -10186,8 +10196,7 @@
     }
 
     /*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) {
-
-        synchronized (VolumeStreamState.class) {
+        synchronized (mVolumeStateLock) {
             sendMsg(mAudioHandler, SoundDoseHelper.MSG_CSD_UPDATE_ATTENUATION, SENDMSG_QUEUE,
                     device, (isAbsoluteVolumeDevice(device) || isA2dpAbsoluteVolumeDevice(device)
                             || AudioSystem.isLeAudioDeviceType(device) ? 1 : 0),
@@ -12191,7 +12200,7 @@
                 mCameraSoundForced = cameraSoundForced;
                 if (cameraSoundForcedChanged) {
                     if (!mIsSingleVolume) {
-                        synchronized (VolumeStreamState.class) {
+                        synchronized (mVolumeStateLock) {
                             final VolumeStreamState s = getVssForStreamOrDefault(
                                     AudioSystem.STREAM_SYSTEM_ENFORCED);
                             if (cameraSoundForced) {
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index e86c34c..a6267c1 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -367,9 +367,9 @@
      * @return
      */
     public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
-            int codecFormat) {
+            int codecFormat, boolean deviceSwitch) {
         invalidateRoutingCache();
-        return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat);
+        return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat, deviceSwitch);
     }
 
     /**
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 9221169..844e352 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -26,6 +26,8 @@
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH;
 
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothA2dp;
@@ -393,8 +395,11 @@
                                 + "received with null profile proxy for device: "
                                 + btDevice)).printLog(TAG));
                 return;
+
             }
-            onSetBtScoActiveDevice(btDevice);
+            boolean deviceSwitch = optimizeBtDeviceSwitch()
+                    && btDevice != null && mBluetoothHeadsetDevice != null;
+            onSetBtScoActiveDevice(btDevice, deviceSwitch);
         } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
             int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
             onScoAudioStateChanged(btState);
@@ -814,7 +819,7 @@
                 if (device == null) {
                     continue;
                 }
-                onSetBtScoActiveDevice(device);
+                onSetBtScoActiveDevice(device, false /*deviceSwitch*/);
             }
         } else {
             Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter");
@@ -907,7 +912,8 @@
     }
 
     @GuardedBy("mDeviceBroker.mDeviceStateLock")
-    private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
+    private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive,
+            boolean deviceSwitch) {
         if (btDevice == null) {
             return true;
         }
@@ -919,12 +925,12 @@
         if (isActive) {
             audioDevice = btHeadsetDeviceToAudioDevice(btDevice);
             result = mDeviceBroker.handleDeviceConnection(
-                    audioDevice, true /*connect*/, btDevice);
+                    audioDevice, true /*connect*/, btDevice, false /*deviceSwitch*/);
         } else {
             AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice);
             if (ada != null) {
                 result = mDeviceBroker.handleDeviceConnection(
-                    ada, false /*connect*/, btDevice);
+                    ada, false /*connect*/, btDevice, deviceSwitch);
             } else {
                 // Disconnect all possible audio device types if the disconnected device type is
                 // unknown
@@ -935,7 +941,8 @@
                 };
                 for (int outDeviceType : outDeviceTypes) {
                     result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                            outDeviceType, address, name), false /*connect*/, btDevice);
+                            outDeviceType, address, name), false /*connect*/, btDevice,
+                            deviceSwitch);
                 }
             }
         }
@@ -944,7 +951,7 @@
         // handleDeviceConnection() && result to make sure the method get executed
         result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
                         inDevice, address, name),
-                isActive, btDevice) && result;
+                isActive, btDevice, deviceSwitch) && result;
         if (result) {
             if (isActive) {
                 mResolvedScoAudioDevices.put(btDevice, audioDevice);
@@ -961,18 +968,18 @@
     }
 
     @GuardedBy("mDeviceBroker.mDeviceStateLock")
-    /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
+    /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) {
         Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice)
-                + " -> " + getAnonymizedAddress(btDevice));
+                + " -> " + getAnonymizedAddress(btDevice) + ", deviceSwitch: " + deviceSwitch);
         final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
         if (Objects.equals(btDevice, previousActiveDevice)) {
             return;
         }
-        if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) {
+        if (!handleBtScoActiveDeviceChange(previousActiveDevice, false, deviceSwitch)) {
             Log.w(TAG, "onSetBtScoActiveDevice() failed to remove previous device "
                     + getAnonymizedAddress(previousActiveDevice));
         }
-        if (!handleBtScoActiveDeviceChange(btDevice, true)) {
+        if (!handleBtScoActiveDeviceChange(btDevice, true, false /*deviceSwitch*/)) {
             Log.e(TAG, "onSetBtScoActiveDevice() failed to add new device "
                     + getAnonymizedAddress(btDevice));
             // set mBluetoothHeadsetDevice to null when failing to add new device
diff --git a/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java b/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java
index 000ee54..9f36467 100644
--- a/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java
+++ b/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java
@@ -20,12 +20,16 @@
 
 import android.annotation.NonNull;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraManager;
+import android.util.Log;
 
 import java.util.concurrent.ConcurrentHashMap;
 
 public class BiometricCameraManagerImpl implements BiometricCameraManager {
 
+    private static final String TAG = "BiometricCameraManager";
+
     private final CameraManager mCameraManager;
     private final SensorPrivacyManager mSensorPrivacyManager;
     private final ConcurrentHashMap<String, Boolean> mIsCameraAvailable = new ConcurrentHashMap<>();
@@ -52,12 +56,18 @@
 
     @Override
     public boolean isAnyCameraUnavailable() {
-        for (String cameraId : mIsCameraAvailable.keySet()) {
-            if (!mIsCameraAvailable.get(cameraId)) {
-                return true;
+        try {
+            for (String cameraId : mCameraManager.getCameraIdList()) {
+                if (!mIsCameraAvailable.getOrDefault(cameraId, true)) {
+                    return true;
+                }
             }
+            return false;
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Camera exception thrown when trying to determine availability: ", e);
+            //If face HAL is unable to get access to a camera, it will return an error.
+            return false;
         }
-        return false;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b6a3f40..258c955 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -812,7 +812,7 @@
             handleMinimalPostProcessingAllowedSettingChange();
 
             if (mFlags.isDisplayContentModeManagementEnabled()) {
-                updateMirrorBuiltInDisplaySettingLocked();
+                updateMirrorBuiltInDisplaySettingLocked(/*shouldSendDisplayChangeEvent=*/ true);
             }
 
             final UserManager userManager = getUserManager();
@@ -868,7 +868,7 @@
                 updateHdrConversionModeSettingsLocked();
             }
             if (mFlags.isDisplayContentModeManagementEnabled()) {
-                updateMirrorBuiltInDisplaySettingLocked();
+                updateMirrorBuiltInDisplaySettingLocked(/*shouldSendDisplayChangeEvent=*/ false);
             }
         }
 
@@ -1237,8 +1237,11 @@
             }
 
             if (Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY).equals(uri)) {
-                if (mFlags.isDisplayContentModeManagementEnabled()) {
-                    updateMirrorBuiltInDisplaySettingLocked();
+                synchronized (mSyncRoot) {
+                    if (mFlags.isDisplayContentModeManagementEnabled()) {
+                        updateMirrorBuiltInDisplaySettingLocked(/*shouldSendDisplayChangeEvent=*/
+                                true);
+                    }
                 }
                 return;
             }
@@ -1258,18 +1261,19 @@
                 1, UserHandle.USER_CURRENT) != 0);
     }
 
-    private void updateMirrorBuiltInDisplaySettingLocked() {
-        synchronized (mSyncRoot) {
-            ContentResolver resolver = mContext.getContentResolver();
-            final boolean mirrorBuiltInDisplay = Settings.Secure.getIntForUser(resolver,
-                    MIRROR_BUILT_IN_DISPLAY, 0, UserHandle.USER_CURRENT) != 0;
-            if (mMirrorBuiltInDisplay == mirrorBuiltInDisplay) {
-                return;
-            }
-            mMirrorBuiltInDisplay = mirrorBuiltInDisplay;
-            if (mFlags.isDisplayContentModeManagementEnabled()) {
-                mLogicalDisplayMapper.forEachLocked(this::updateCanHostTasksIfNeededLocked);
-            }
+    private void updateMirrorBuiltInDisplaySettingLocked(boolean shouldSendDisplayChangeEvent) {
+        ContentResolver resolver = mContext.getContentResolver();
+        final boolean mirrorBuiltInDisplay = Settings.Secure.getIntForUser(resolver,
+                MIRROR_BUILT_IN_DISPLAY, 0, UserHandle.USER_CURRENT) != 0;
+        if (mMirrorBuiltInDisplay == mirrorBuiltInDisplay) {
+            return;
+        }
+        mMirrorBuiltInDisplay = mirrorBuiltInDisplay;
+        if (mFlags.isDisplayContentModeManagementEnabled()) {
+            mLogicalDisplayMapper.forEachLocked(logicalDisplay -> {
+                    updateCanHostTasksIfNeededLocked(logicalDisplay,
+                            shouldSendDisplayChangeEvent);
+            });
         }
     }
 
@@ -2380,7 +2384,7 @@
                 new BrightnessPair(brightnessDefault, brightnessDefault));
 
         if (mFlags.isDisplayContentModeManagementEnabled()) {
-            updateCanHostTasksIfNeededLocked(display);
+            updateCanHostTasksIfNeededLocked(display, /*shouldSendDisplayChangeEvent=*/ false);
         }
 
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
@@ -2587,6 +2591,11 @@
         sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED);
     }
 
+    private void handleLogicalDisplayCommittedStateChangedLocked(@NonNull LogicalDisplay display) {
+        sendDisplayEventIfEnabledLocked(display,
+                DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED);
+    }
+
     private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) {
         mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked()
                 .mDisplayDeviceConfig);
@@ -2609,7 +2618,8 @@
         // Blank or unblank the display immediately to match the state requested
         // by the display power controller (if known).
         DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
+        if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0
+                || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
             if (display == null) {
                 return null;
@@ -2697,8 +2707,9 @@
         }
     }
 
-    private void updateCanHostTasksIfNeededLocked(LogicalDisplay display) {
-        if (display.setCanHostTasksLocked(!mMirrorBuiltInDisplay)) {
+    private void updateCanHostTasksIfNeededLocked(LogicalDisplay display,
+            boolean shouldSendDisplayChangeEvent) {
+        if (display.setCanHostTasksLocked(!mMirrorBuiltInDisplay) && shouldSendDisplayChangeEvent) {
             sendDisplayEventIfEnabledLocked(display,
                     DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED);
         }
@@ -4165,6 +4176,9 @@
                 case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED:
                     handleLogicalDisplayStateChangedLocked(display);
                     break;
+                case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED:
+                    handleLogicalDisplayCommittedStateChangedLocked(display);
+                    break;
             }
         }
 
@@ -4419,6 +4433,9 @@
                 case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED:
                     return (mask & DisplayManagerGlobal
                             .INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0;
+                case DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+                    return (mask & DisplayManagerGlobal
+                            .INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0;
                 default:
                     // This should never happen.
                     Slog.e(TAG, "Unknown display event " + event);
@@ -5563,7 +5580,9 @@
                     final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked(
                             id).getPrimaryDisplayDeviceLocked();
                     final int flags = displayDevice.getDisplayDeviceInfoLocked().flags;
-                    if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
+                    if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0
+                            || android.companion.virtualdevice.flags.Flags
+                                    .correctVirtualDisplayPowerState()) {
                         final DisplayPowerController displayPowerController =
                                 mDisplayPowerControllers.get(id);
                         if (displayPowerController != null) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 02db051..872f334 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -91,6 +91,8 @@
     public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 1 << 8;
     public static final int LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED = 1 << 9;
     public static final int LOGICAL_DISPLAY_EVENT_STATE_CHANGED = 1 << 10;
+    public static final int LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED = 1 << 11;
+
 
     public static final int DISPLAY_GROUP_EVENT_ADDED = 1;
     public static final int DISPLAY_GROUP_EVENT_CHANGED = 2;
@@ -810,7 +812,7 @@
             int logicalDisplayEventMask = mLogicalDisplaysToUpdate
                     .get(displayId, LOGICAL_DISPLAY_EVENT_BASE);
             boolean hasBasicInfoChanged =
-                    !mTempDisplayInfo.equals(newDisplayInfo, /* compareRefreshRate */ false);
+                    !mTempDisplayInfo.equals(newDisplayInfo, /* compareOnlyBasicChanges */ true);
             // The display is no longer valid and needs to be removed.
             if (!display.isValidLocked()) {
                 // Remove from group
@@ -930,6 +932,7 @@
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED);
+        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED);
@@ -961,6 +964,11 @@
                 && mTempDisplayInfo.state != newDisplayInfo.state) {
             mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED;
         }
+
+        if (mFlags.isCommittedStateSeparateEventEnabled()
+                && mTempDisplayInfo.committedState != newDisplayInfo.committedState) {
+            mask |= LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED;
+        }
         return mask;
     }
     /**
@@ -1360,6 +1368,8 @@
                 return "disconnected";
             case LOGICAL_DISPLAY_EVENT_STATE_CHANGED:
                 return "state_changed";
+            case LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED:
+                return "committed_state_changed";
             case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED:
                 return "refresh_rate_changed";
             case LOGICAL_DISPLAY_EVENT_BASIC_CHANGED:
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 4779b69..e7939bb 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -371,7 +371,15 @@
             mCallback = callback;
             mProjection = projection;
             mMediaProjectionCallback = mediaProjectionCallback;
-            mDisplayState = Display.STATE_ON;
+            if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
+                // The display's power state depends on the power state of the state of its
+                // display / power group, which we don't know here. Initializing to UNKNOWN allows
+                // the first call to requestDisplayStateLocked() to set the correct state.
+                // This also triggers VirtualDisplay.Callback to tell the owner the initial state.
+                mDisplayState = Display.STATE_UNKNOWN;
+            } else {
+                mDisplayState = Display.STATE_ON;
+            }
             mPendingChanges |= PENDING_SURFACE_CHANGE;
             mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror();
             mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled();
@@ -564,14 +572,23 @@
                 mInfo.yDpi = mDensityDpi;
                 mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame
                 mInfo.flags = 0;
-                if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
-                    mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
-                            | DisplayDeviceInfo.FLAG_NEVER_BLANK;
-                }
-                if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
-                    mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK;
+                if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
+                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
+                    }
+                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+                    }
                 } else {
-                    mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
+                                | DisplayDeviceInfo.FLAG_NEVER_BLANK;
+                    }
+                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
+                        mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK;
+                    } else {
+                        mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+                    }
                 }
                 if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
                     mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index bc5d905..e4b595a 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -280,6 +280,11 @@
             Flags::refreshRateEventForForegroundApps
     );
 
+    private final FlagState mCommittedStateSeparateEvent = new FlagState(
+            Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT,
+            Flags::committedStateSeparateEvent
+    );
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -603,6 +608,14 @@
     }
 
     /**
+     * @return {@code true} if the flag for having a separate event for display's committed state
+     * is enabled
+     */
+    public boolean isCommittedStateSeparateEventEnabled() {
+        return mCommittedStateSeparateEvent.isEnabled();
+    }
+
+    /**
      * dumps all flagstates
      * @param pw printWriter
      */
@@ -659,6 +672,7 @@
         pw.println(" " + mBaseDensityForExternalDisplays);
         pw.println(" " + mFramerateOverrideTriggersRrCallbacks);
         pw.println(" " + mRefreshRateEventForForegroundApps);
+        pw.println(" " + mCommittedStateSeparateEvent);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 8211feb..acdc0e0 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -456,9 +456,8 @@
 flag {
     name: "enable_display_content_mode_management"
     namespace: "lse_desktop_experience"
-    description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode."
+    description: "Enable switching the content mode of connected displays between mirroring and extended. Also change the default content mode to extended mode."
     bug: "378385869"
-    is_fixed_read_only: true
 }
 
 flag {
@@ -509,3 +508,14 @@
     bug: "293651324"
     is_fixed_read_only: false
 }
+
+flag {
+    name: "committed_state_separate_event"
+    namespace: "display_manager"
+    description: "Move Display committed state into a separate event"
+    bug: "342192387"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 2af74f6..7e8bb28 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -569,8 +569,7 @@
     }
 
     private void requestDreamInternal() {
-        if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()
-                && !isDozingInternal()) {
+        if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) {
             return;
         }
 
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
index 9484204..ab86433 100644
--- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
@@ -58,9 +58,9 @@
     void setDeviceAbsoluteVolumeBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment);
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener);
 
     /**
      * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@@ -69,7 +69,7 @@
     void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment);
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener);
 }
diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
index ff99ace..10cbb00 100644
--- a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
+++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
@@ -61,21 +61,21 @@
     public void setDeviceAbsoluteVolumeBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
-        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
-                vclistener, handlesVolumeAdjustment);
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) {
+        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume,
+                handlesVolumeAdjustment, executor, vclistener);
     }
 
     @Override
     public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
+            boolean handlesVolumeAdjustment,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) {
         mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume,
-                executor, vclistener, handlesVolumeAdjustment);
+                handlesVolumeAdjustment, executor, vclistener);
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 89f0d0e..6d973ac 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4798,15 +4798,15 @@
             Slog.d(TAG, "Enabling absolute volume behavior");
             for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
                 getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
-                        device, volumeInfo, mServiceThreadExecutor,
-                        mAbsoluteVolumeChangedListener, true);
+                        device, volumeInfo, true, mServiceThreadExecutor,
+                        mAbsoluteVolumeChangedListener);
             }
         } else if (tv() != null) {
             Slog.d(TAG, "Enabling adjust-only absolute volume behavior");
             for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
                 getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior(
-                        device, volumeInfo, mServiceThreadExecutor,
-                        mAbsoluteVolumeChangedListener, true);
+                        device, volumeInfo, true, mServiceThreadExecutor,
+                        mAbsoluteVolumeChangedListener);
             }
         }
 
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 87f693c..1ace41c 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -344,4 +344,42 @@
      */
     public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
             throws XmlPullParserException, IOException;
+
+    /**
+     * An interface for filtering pointer motion event before cursor position is determined.
+     * <p>
+     * Different from {@code android.view.InputFilter}, this filter can filter motion events at
+     * an early stage of the input pipeline, but only called for pointer's relative motion events.
+     * Unless the user really needs to filter events before the cursor position in the display is
+     * determined, use {@code android.view.InputFilter} instead.
+     */
+    public interface AccessibilityPointerMotionFilter {
+        /**
+         * Called everytime pointer's relative motion event happens.
+         * The returned dx and dy will be used to move the cursor in the display.
+         * <p>
+         * This call happens on the input hot path and it is extremely performance sensitive. It
+         * also must not call back into native code.
+         *
+         * @param dx        delta x of the event in pixels.
+         * @param dy        delta y of the event in pixels.
+         * @param currentX  the cursor x coordinate on the screen before the motion event.
+         * @param currentY  the cursor y coordinate on the screen before the motion event.
+         * @param displayId the display ID of the current cursor.
+         * @return an array of length 2, delta x and delta y after filtering the motion. The delta
+         *         values are in pixels and must be between 0 and original delta.
+         */
+        @NonNull
+        float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY,
+                int displayId);
+    }
+
+    /**
+     * Registers an {@code AccessibilityCursorFilter}.
+     *
+     * @param filter The filter to register. If a filter is already registered, the old filter is
+     *               unregistered. {@code null} unregisters the filter that is already registered.
+     */
+    public abstract void registerAccessibilityPointerMotionFilter(
+            @Nullable AccessibilityPointerMotionFilter filter);
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 8624f42..0e37238 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,8 +25,8 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
 import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
-import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.keyEventActivityDetection;
+import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
 import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
 
@@ -193,15 +193,11 @@
     private static final int MSG_SYSTEM_READY = 5;
 
     private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
-    private static final AdditionalDisplayInputProperties
-            DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
 
     private final NativeInputManagerService mNative;
 
     private final Context mContext;
     private final InputManagerHandler mHandler;
-    @UserIdInt
-    private int mCurrentUserId = UserHandle.USER_SYSTEM;
     private DisplayManagerInternal mDisplayManagerInternal;
 
     private WindowManagerInternal mWindowManagerInternal;
@@ -289,7 +285,7 @@
 
     final Object mKeyEventActivityLock = new Object();
     @GuardedBy("mKeyEventActivityLock")
-    private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify =
+    private final List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify =
             new ArrayList<>();
 
     // Rate limit for key event activity detection. Prevent the listener from being notified
@@ -460,6 +456,14 @@
     private boolean mShowKeyPresses = false;
     private boolean mShowRotaryInput = false;
 
+    /**
+     * A lock for the accessibility pointer motion filter. Don't call native methods while holding
+     * this lock.
+     */
+    private final Object mAccessibilityPointerMotionFilterLock = new Object();
+    private InputManagerInternal.AccessibilityPointerMotionFilter
+            mAccessibilityPointerMotionFilter = null;
+
     /** Point of injection for test dependencies. */
     @VisibleForTesting
     static class Injector {
@@ -2593,6 +2597,23 @@
 
     // Native callback.
     @SuppressWarnings("unused")
+    final float[] filterPointerMotion(float dx, float dy, float currentX, float currentY,
+            int displayId) {
+        // This call happens on the input hot path and it is extremely performance sensitive.
+        // This must not call back into native code. This is called while the
+        // PointerChoreographer's lock is held.
+        synchronized (mAccessibilityPointerMotionFilterLock) {
+            if (mAccessibilityPointerMotionFilter == null) {
+                throw new IllegalStateException(
+                        "filterCursor is invoked but no callback is registered.");
+            }
+            return mAccessibilityPointerMotionFilter.filterPointerMotionEvent(dx, dy, currentX,
+                    currentY, displayId);
+        }
+    }
+
+    // Native callback.
+    @SuppressWarnings("unused")
     @VisibleForTesting
     public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
         notifyKeyActivityListeners(event);
@@ -3215,7 +3236,6 @@
     }
 
     private void handleCurrentUserChanged(@UserIdInt int userId) {
-        mCurrentUserId = userId;
         mKeyGestureController.setCurrentUserId(userId);
     }
 
@@ -3828,6 +3848,12 @@
                         payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId);
             }
         }
+
+        @Override
+        public void registerAccessibilityPointerMotionFilter(
+                AccessibilityPointerMotionFilter filter) {
+            InputManagerService.this.registerAccessibilityPointerMotionFilter(filter);
+        }
     }
 
     @Override
@@ -4014,6 +4040,26 @@
         mPointerIconCache.setAccessibilityScaleFactor(displayId, scaleFactor);
     }
 
+    void registerAccessibilityPointerMotionFilter(
+            InputManagerInternal.AccessibilityPointerMotionFilter filter) {
+        // `#filterPointerMotion` expects that when it's called, `mAccessibilityPointerMotionFilter`
+        // is not null.
+        // Also, to avoid potential lock contention, we shouldn't call native method while holding
+        // the lock here. Native code calls `#filterPointerMotion` while PointerChoreographer's
+        // lock is held.
+        // Thus, we must set filter before we enable the filter in native, and reset the filter
+        // after we disable the filter.
+        // This also ensures the previously installed filter isn't called after the filter is
+        // updated.
+        mNative.setAccessibilityPointerMotionFilterEnabled(false);
+        synchronized (mAccessibilityPointerMotionFilterLock) {
+            mAccessibilityPointerMotionFilter = filter;
+        }
+        if (filter != null) {
+            mNative.setAccessibilityPointerMotionFilterEnabled(true);
+        }
+    }
+
     interface KeyboardBacklightControllerInterface {
         default void incrementKeyboardBacklight(int deviceId) {}
         default void decrementKeyboardBacklight(int deviceId) {}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index f34338a..32409d3 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -315,6 +315,16 @@
      */
     boolean setKernelWakeEnabled(int deviceId, boolean enabled);
 
+    /**
+     * Set whether the accessibility pointer motion filter is enabled.
+     * <p>
+     * Once enabled, {@link InputManagerService#filterPointerMotion} is called for evety motion
+     * event from pointer devices.
+     *
+     * @param enabled {@code true} if the filter is enabled, {@code false} otherwise.
+     */
+    void setAccessibilityPointerMotionFilterEnabled(boolean enabled);
+
     /** The native implementation of InputManagerService methods. */
     class NativeImpl implements NativeInputManagerService {
         /** Pointer to native input manager service object, used by native code. */
@@ -628,5 +638,8 @@
 
         @Override
         public native boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
+        @Override
+        public native void setAccessibilityPointerMotionFilterEnabled(boolean enabled);
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 484b470..508bc2f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -365,7 +365,7 @@
         return mCurrentImeUserId;
     }
 
-   /**
+    /**
      * Figures out the target IME user ID associated with the given {@code displayId}.
      *
      * @param displayId the display ID to be queried about
@@ -649,12 +649,25 @@
                 visibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
                         accessibilitySoftKeyboardSetting);
                 if (visibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
-                    hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
-                            0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId);
+                    if (Flags.refactorInsetsController()) {
+                        final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+                                SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId);
+                        setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+                    } else {
+                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE,
+                                userId);
+                    }
                 } else if (isShowRequestedForCurrentWindow(userId)) {
-                    showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
-                            InputMethodManager.SHOW_IMPLICIT,
-                            SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+                    if (Flags.refactorInsetsController()) {
+                        final var statsToken = createStatsTokenForFocusedClient(true /* show */,
+                                SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+                        setImeVisibilityOnFocusedWindowClient(true, userData, statsToken);
+                    } else {
+                        showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                InputMethodManager.SHOW_IMPLICIT,
+                                SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+                    }
                 }
                 break;
             }
@@ -1319,8 +1332,8 @@
         // Do not reset the default (current) IME when it is a 3rd-party IME
         String selectedMethodId = bindingController.getSelectedMethodId();
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
-        if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null
-                && !settings.getMethodMap().get(selectedMethodId).isSystem()) {
+        final InputMethodInfo selectedImi = settings.getMethodMap().get(selectedMethodId);
+        if (selectedImi != null && !selectedImi.isSystem()) {
             return;
         }
         final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes(
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 4cf4396..91a2843 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -120,6 +120,8 @@
     private final Object mPictureProfileLock = new Object();
     // A global lock for sound profile objects.
     private final Object mSoundProfileLock = new Object();
+    // A global lock for user state objects.
+    private final Object mUserStateLock = new Object();
     // A global lock for ambient backlight objects.
     private final Object mAmbientBacklightLock = new Object();
 
@@ -181,7 +183,6 @@
         publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
     }
 
-    // TODO: Add additional APIs. b/373951081
     private final class BinderService extends IMediaQualityManager.Stub {
 
         @GuardedBy("mPictureProfileLock")
@@ -269,12 +270,13 @@
                         mMqManagerNotifier.notifyOnPictureProfileError(id,
                                 PictureProfile.ERROR_INVALID_ARGUMENT,
                                 Binder.getCallingUid(), Binder.getCallingPid());
+                    } else {
+                        mMqManagerNotifier.notifyOnPictureProfileRemoved(
+                                mPictureProfileTempIdMap.getValue(dbId), toDelete,
+                                Binder.getCallingUid(), Binder.getCallingPid());
+                        mPictureProfileTempIdMap.remove(dbId);
+                        mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
                     }
-                    mMqManagerNotifier.notifyOnPictureProfileRemoved(
-                            mPictureProfileTempIdMap.getValue(dbId), toDelete,
-                            Binder.getCallingUid(), Binder.getCallingPid());
-                    mPictureProfileTempIdMap.remove(dbId);
-                    mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
                 }
             }
         }
@@ -520,12 +522,13 @@
                         mMqManagerNotifier.notifyOnSoundProfileError(id,
                                 SoundProfile.ERROR_INVALID_ARGUMENT,
                                 Binder.getCallingUid(), Binder.getCallingPid());
+                    } else {
+                        mMqManagerNotifier.notifyOnSoundProfileRemoved(
+                                mSoundProfileTempIdMap.getValue(dbId), toDelete,
+                                Binder.getCallingUid(), Binder.getCallingPid());
+                        mSoundProfileTempIdMap.remove(dbId);
+                        mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
                     }
-                    mMqManagerNotifier.notifyOnSoundProfileRemoved(
-                            mSoundProfileTempIdMap.getValue(dbId), toDelete,
-                            Binder.getCallingUid(), Binder.getCallingPid());
-                    mSoundProfileTempIdMap.remove(dbId);
-                    mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
                 }
             }
         }
@@ -684,24 +687,22 @@
                     mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
         }
 
-        //TODO: need lock here?
         @Override
         public void registerPictureProfileCallback(final IPictureProfileCallback callback) {
             int callingPid = Binder.getCallingPid();
             int callingUid = Binder.getCallingUid();
 
-            UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid());
+            UserState userState = getOrCreateUserState(Binder.getCallingUid());
             userState.mPictureProfileCallbackPidUidMap.put(callback,
                     Pair.create(callingPid, callingUid));
         }
 
-        //TODO: need lock here?
         @Override
         public void registerSoundProfileCallback(final ISoundProfileCallback callback) {
             int callingPid = Binder.getCallingPid();
             int callingUid = Binder.getCallingUid();
 
-            UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid());
+            UserState userState = getOrCreateUserState(Binder.getCallingUid());
             userState.mSoundProfileCallbackPidUidMap.put(callback,
                     Pair.create(callingPid, callingUid));
         }
@@ -1060,7 +1061,7 @@
             synchronized (mPictureProfileLock) {
                 for (int i = 0; i < mUserStates.size(); i++) {
                     int userId = mUserStates.keyAt(i);
-                    UserState userState = getOrCreateUserStateLocked(userId);
+                    UserState userState = getOrCreateUserState(userId);
                     userState.mPictureProfileCallbackPidUidMap.remove(callback);
                 }
             }
@@ -1074,7 +1075,7 @@
             synchronized (mSoundProfileLock) {
                 for (int i = 0; i < mUserStates.size(); i++) {
                     int userId = mUserStates.keyAt(i);
-                    UserState userState = getOrCreateUserStateLocked(userId);
+                    UserState userState = getOrCreateUserState(userId);
                     userState.mSoundProfileCallbackPidUidMap.remove(callback);
                 }
             }
@@ -1100,19 +1101,23 @@
         }
     }
 
-    //TODO: used by both picture and sound. can i add both locks?
-    private UserState getOrCreateUserStateLocked(int userId) {
-        UserState userState = getUserStateLocked(userId);
+    @GuardedBy("mUserStateLock")
+    private UserState getOrCreateUserState(int userId) {
+        UserState userState = getUserState(userId);
         if (userState == null) {
             userState = new UserState(mContext, userId);
-            mUserStates.put(userId, userState);
+            synchronized (mUserStateLock) {
+                mUserStates.put(userId, userState);
+            }
         }
         return userState;
     }
 
-    //TODO: used by both picture and sound. can i add both locks?
-    private UserState getUserStateLocked(int userId) {
-        return mUserStates.get(userId);
+    @GuardedBy("mUserStateLock")
+    private UserState getUserState(int userId) {
+        synchronized (mUserStateLock) {
+            return mUserStates.get(userId);
+        }
     }
 
     private final class MqDatabaseUtils {
@@ -1266,7 +1271,7 @@
         private void notifyPictureProfileHelper(int mode, String profileId,
                 PictureProfile profile, Integer errorCode,
                 List<ParameterCapability> paramCaps, int uid, int pid) {
-            UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM);
+            UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
             int n = userState.mPictureProfileCallbacks.beginBroadcast();
 
             for (int i = 0; i < n; ++i) {
@@ -1351,7 +1356,7 @@
         private void notifySoundProfileHelper(int mode, String profileId,
                 SoundProfile profile, Integer errorCode,
                 List<ParameterCapability> paramCaps, int uid, int pid) {
-            UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM);
+            UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
             int n = userState.mSoundProfileCallbacks.beginBroadcast();
 
             for (int i = 0; i < n; ++i) {
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index b0ef807..62e26e1 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -25,6 +25,8 @@
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
 
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
 import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
 
 import android.annotation.FlaggedApi;
@@ -75,7 +77,9 @@
 import com.android.internal.util.function.TriPredicate;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.LocalServices;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.utils.TimingsTraceAndSlog;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -134,6 +138,7 @@
     private final UserProfiles mUserProfiles;
     protected final IPackageManager mPm;
     protected final UserManager mUm;
+    protected final UserManagerInternal mUmInternal;
     private final Config mConfig;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
@@ -157,12 +162,17 @@
     protected final ArraySet<String> mDefaultPackages = new ArraySet<>();
 
     // lists the component names of all enabled (and therefore potentially connected)
-    // app services for current profiles.
+    // app services for each user. This is intended to support a concurrent multi-user environment.
+    // key value is the resolved userId.
     @GuardedBy("mMutex")
-    private final ArraySet<ComponentName> mEnabledServicesForCurrentProfiles = new ArraySet<>();
-    // Just the packages from mEnabledServicesForCurrentProfiles
+    private final SparseArray<ArraySet<ComponentName>> mEnabledServicesByUser =
+            new SparseArray<>();
+    // Just the packages from mEnabledServicesByUser
+    // This is intended to support a concurrent multi-user environment.
+    // key value is the resolved userId.
     @GuardedBy("mMutex")
-    private final ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
+    private final SparseArray<ArraySet<String>> mEnabledServicesPackageNamesByUser =
+            new SparseArray<>();
     // Per user id, list of enabled packages that have nevertheless asked not to be run
     @GuardedBy("mSnoozing")
     private final SparseSetArray<ComponentName> mSnoozing = new SparseSetArray<>();
@@ -195,6 +205,10 @@
         mConfig = getConfig();
         mApprovalLevel = APPROVAL_BY_COMPONENT;
         mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mUmInternal = LocalServices.getService(UserManagerInternal.class);
+        // Initialize for the current user.
+        mEnabledServicesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>());
+        mEnabledServicesPackageNamesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>());
     }
 
     abstract protected Config getConfig();
@@ -383,11 +397,30 @@
         }
 
         synchronized (mMutex) {
-            pw.println("    All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size()
-                    + ") enabled for current profiles:");
-            for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
-                if (filter != null && !filter.matches(cmpt)) continue;
-                pw.println("      " + cmpt);
+            if (managedServicesConcurrentMultiuser()) {
+                for (int i = 0; i < mEnabledServicesByUser.size(); i++) {
+                    final int userId = mEnabledServicesByUser.keyAt(i);
+                    final ArraySet<ComponentName> componentNames =
+                            mEnabledServicesByUser.get(userId);
+                    String userString = userId == UserHandle.USER_CURRENT
+                            ? "current profiles" : "user " + Integer.toString(userId);
+                    pw.println("    All " + getCaption() + "s (" + componentNames.size()
+                            + ") enabled for " +  userString + ":");
+                    for (ComponentName cmpt : componentNames) {
+                        if (filter != null && !filter.matches(cmpt)) continue;
+                        pw.println("      " + cmpt);
+                    }
+                }
+            } else {
+                final ArraySet<ComponentName> enabledServicesForCurrentProfiles =
+                        mEnabledServicesByUser.get(UserHandle.USER_CURRENT);
+                pw.println("    All " + getCaption() + "s ("
+                        + enabledServicesForCurrentProfiles.size()
+                        + ") enabled for current profiles:");
+                for (ComponentName cmpt : enabledServicesForCurrentProfiles) {
+                    if (filter != null && !filter.matches(cmpt)) continue;
+                    pw.println("      " + cmpt);
+                }
             }
 
             pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
@@ -442,11 +475,24 @@
             }
         }
 
-
         synchronized (mMutex) {
-            for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
-                if (filter != null && !filter.matches(cmpt)) continue;
-                cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+            if (managedServicesConcurrentMultiuser()) {
+                for (int i = 0; i < mEnabledServicesByUser.size(); i++) {
+                    final int userId = mEnabledServicesByUser.keyAt(i);
+                    final ArraySet<ComponentName> componentNames =
+                            mEnabledServicesByUser.get(userId);
+                    for (ComponentName cmpt : componentNames) {
+                        if (filter != null && !filter.matches(cmpt)) continue;
+                        cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+                    }
+                }
+            } else {
+                final ArraySet<ComponentName> enabledServicesForCurrentProfiles =
+                        mEnabledServicesByUser.get(UserHandle.USER_CURRENT);
+                for (ComponentName cmpt : enabledServicesForCurrentProfiles) {
+                    if (filter != null && !filter.matches(cmpt)) continue;
+                    cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+                }
             }
             for (ManagedServiceInfo info : mServices) {
                 if (filter != null && !filter.matches(info.component)) continue;
@@ -841,9 +887,31 @@
         }
     }
 
+    /** convenience method for looking in mEnabledServicesPackageNamesByUser
+     * for UserHandle.USER_CURRENT.
+     * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes
+     * trunk stable,  this API should be deprecated.  Additionally, when this method
+     * is deprecated, the unit tests written using this method should also be revised.
+     *
+     * @param pkg target package name
+     * @return boolean value that indicates whether it is enabled for the current profiles
+     */
     protected boolean isComponentEnabledForPackage(String pkg) {
+        return isComponentEnabledForPackage(pkg, UserHandle.USER_CURRENT);
+    }
+
+    /** convenience method for looking in mEnabledServicesPackageNamesByUser
+     *
+     * @param pkg target package name
+     * @param userId the id of the target user
+     * @return boolean value that indicates whether it is enabled for the target user
+     */
+    @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    protected boolean isComponentEnabledForPackage(String pkg, int userId) {
         synchronized (mMutex) {
-            return mEnabledServicesPackageNames.contains(pkg);
+            ArraySet<String> enabledServicesPackageNames =
+                    mEnabledServicesPackageNamesByUser.get(resolveUserId(userId));
+            return enabledServicesPackageNames != null && enabledServicesPackageNames.contains(pkg);
         }
     }
 
@@ -1016,9 +1084,14 @@
     public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) {
         if (DEBUG) {
             synchronized (mMutex) {
+                int resolvedUserId = (managedServicesConcurrentMultiuser()
+                        && (uidList != null && uidList.length > 0))
+                        ? resolveUserId(UserHandle.getUserId(uidList[0]))
+                        : UserHandle.USER_CURRENT;
                 Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage
                         + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
-                        + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames);
+                        + " mEnabledServicesPackageNames="
+                        + mEnabledServicesPackageNamesByUser.get(resolvedUserId));
             }
         }
 
@@ -1034,11 +1107,18 @@
                 }
             }
             for (String pkgName : pkgList) {
-                if (isComponentEnabledForPackage(pkgName)) {
-                    anyServicesInvolved = true;
+                if (!managedServicesConcurrentMultiuser()) {
+                    if (isComponentEnabledForPackage(pkgName)) {
+                        anyServicesInvolved = true;
+                    }
                 }
                 if (uidList != null && uidList.length > 0) {
                     for (int uid : uidList) {
+                        if (managedServicesConcurrentMultiuser()) {
+                            if (isComponentEnabledForPackage(pkgName, UserHandle.getUserId(uid))) {
+                                anyServicesInvolved = true;
+                            }
+                        }
                         if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) {
                             anyServicesInvolved = true;
                             trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid));
@@ -1065,6 +1145,36 @@
         unbindUserServices(user);
     }
 
+    /**
+     * Call this method when a user is stopped
+     *
+     * @param user the id of the stopped user
+     */
+    public void onUserStopped(int user) {
+        if (!managedServicesConcurrentMultiuser()) {
+            return;
+        }
+        boolean hasAny = false;
+        synchronized (mMutex) {
+            if (mEnabledServicesByUser.contains(user)
+                    && mEnabledServicesPackageNamesByUser.contains(user)) {
+                // Through the ManagedServices.resolveUserId,
+                // we resolve UserHandle.USER_CURRENT as the key for users
+                // other than the visible background user.
+                // Therefore, the user IDs that exist as keys for each member variable
+                // correspond to the visible background user.
+                // We need to unbind services of the stopped visible background user.
+                mEnabledServicesByUser.remove(user);
+                mEnabledServicesPackageNamesByUser.remove(user);
+                hasAny = true;
+            }
+        }
+        if (hasAny) {
+            Slog.i(TAG, "Removing approved services for stopped user " + user);
+            unbindUserServices(user);
+        }
+    }
+
     public void onUserSwitched(int user) {
         if (DEBUG) Slog.d(TAG, "onUserSwitched u=" + user);
         unbindOtherUserServices(user);
@@ -1386,19 +1496,42 @@
     protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind,
             final IntArray activeUsers,
             SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) {
-        mEnabledServicesForCurrentProfiles.clear();
-        mEnabledServicesPackageNames.clear();
         final int nUserIds = activeUsers.size();
-
+        if (managedServicesConcurrentMultiuser()) {
+            for (int i = 0; i < nUserIds; ++i) {
+                final int resolvedUserId = resolveUserId(activeUsers.get(i));
+                if (mEnabledServicesByUser.get(resolvedUserId) != null) {
+                    mEnabledServicesByUser.get(resolvedUserId).clear();
+                }
+                if (mEnabledServicesPackageNamesByUser.get(resolvedUserId) != null) {
+                    mEnabledServicesPackageNamesByUser.get(resolvedUserId).clear();
+                }
+            }
+        } else {
+            mEnabledServicesByUser.get(UserHandle.USER_CURRENT).clear();
+            mEnabledServicesPackageNamesByUser.get(UserHandle.USER_CURRENT).clear();
+        }
         for (int i = 0; i < nUserIds; ++i) {
-            // decode the list of components
             final int userId = activeUsers.get(i);
+            // decode the list of components
             final ArraySet<ComponentName> userComponents = approvedComponentsByUser.get(userId);
             if (null == userComponents) {
                 componentsToBind.put(userId, new ArraySet<>());
                 continue;
             }
 
+            final int resolvedUserId = managedServicesConcurrentMultiuser()
+                    ? resolveUserId(userId)
+                    : UserHandle.USER_CURRENT;
+            ArraySet<ComponentName> enabledServices =
+                    mEnabledServicesByUser.contains(resolvedUserId)
+                    ? mEnabledServicesByUser.get(resolvedUserId)
+                    : new ArraySet<>();
+            ArraySet<String> enabledServicesPackageName =
+                    mEnabledServicesPackageNamesByUser.contains(resolvedUserId)
+                    ? mEnabledServicesPackageNamesByUser.get(resolvedUserId)
+                    : new ArraySet<>();
+
             final Set<ComponentName> add = new HashSet<>(userComponents);
             synchronized (mSnoozing) {
                 ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
@@ -1409,12 +1542,12 @@
 
             componentsToBind.put(userId, add);
 
-            mEnabledServicesForCurrentProfiles.addAll(userComponents);
-
+            enabledServices.addAll(userComponents);
             for (int j = 0; j < userComponents.size(); j++) {
-                final ComponentName component = userComponents.valueAt(j);
-                mEnabledServicesPackageNames.add(component.getPackageName());
+                enabledServicesPackageName.add(userComponents.valueAt(j).getPackageName());
             }
+            mEnabledServicesByUser.put(resolvedUserId, enabledServices);
+            mEnabledServicesPackageNamesByUser.put(resolvedUserId, enabledServicesPackageName);
         }
     }
 
@@ -1453,13 +1586,9 @@
      */
     protected void rebindServices(boolean forceRebind, int userToRebind) {
         if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind);
-        IntArray userIds = mUserProfiles.getCurrentProfileIds();
         boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext)
                 && allowRebindForParentUser();
-        if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
-            userIds = new IntArray(1);
-            userIds.add(userToRebind);
-        }
+        IntArray userIds = getUserIdsForRebindServices(userToRebind, rebindAllCurrentUsers);
 
         final SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
         final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
@@ -1483,6 +1612,23 @@
         bindToServices(componentsToBind);
     }
 
+    private IntArray getUserIdsForRebindServices(int userToRebind, boolean rebindAllCurrentUsers) {
+        IntArray userIds = mUserProfiles.getCurrentProfileIds();
+        if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
+            userIds = new IntArray(1);
+            userIds.add(userToRebind);
+        } else if (managedServicesConcurrentMultiuser()
+                && userToRebind == USER_ALL) {
+            for (UserInfo user : mUm.getUsers()) {
+                if (mUmInternal.isVisibleBackgroundFullUser(user.id)
+                        && !userIds.contains(user.id)) {
+                    userIds.add(user.id);
+                }
+            }
+        }
+        return userIds;
+    }
+
     /**
      * Called when user switched to unbind all services from other users.
      */
@@ -1506,7 +1652,11 @@
         synchronized (mMutex) {
             final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices();
             for (ManagedServiceInfo info : removableBoundServices) {
-                if ((allExceptUser && (info.userid != user))
+                // User switching is the event for the forground user.
+                // It should not affect the service of the visible background user.
+                if ((allExceptUser && (info.userid != user)
+                        && !(managedServicesConcurrentMultiuser()
+                            && info.isVisibleBackgroundUserService))
                         || (!allExceptUser && (info.userid == user))) {
                     Set<ComponentName> toUnbind =
                             componentsToUnbind.get(info.userid, new ArraySet<>());
@@ -1861,6 +2011,29 @@
     }
 
     /**
+     * This method returns the mapped id for the incoming user id
+     * If the incoming id was not the id of the visible background user, it returns USER_CURRENT.
+     * In the other cases, it returns the same value as the input.
+     *
+     * @param userId the id of the user
+     * @return the user id if it is a visible background user, otherwise
+     * {@link UserHandle#USER_CURRENT}
+     */
+    @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    @VisibleForTesting
+    public int resolveUserId(int userId) {
+        if (managedServicesConcurrentMultiuser()) {
+            if (mUmInternal.isVisibleBackgroundFullUser(userId)) {
+                // The dataset of the visible background user should be managed independently.
+                return userId;
+            }
+        }
+        // The data of current user and its profile users need to  be managed
+        // in a dataset as before.
+        return UserHandle.USER_CURRENT;
+    }
+
+    /**
      * Returns true if services in the parent user should be rebound
      *  when rebindServices is called with a profile userId.
      * Must be false for NotificationAssistants.
@@ -1878,6 +2051,8 @@
         public int targetSdkVersion;
         public Pair<ComponentName, Integer> mKey;
         public int uid;
+        @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+        public boolean isVisibleBackgroundUserService;
 
         public ManagedServiceInfo(IInterface service, ComponentName component,
                 int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion,
@@ -1889,6 +2064,10 @@
             this.connection = connection;
             this.targetSdkVersion = targetSdkVersion;
             this.uid = uid;
+            if (managedServicesConcurrentMultiuser()) {
+                this.isVisibleBackgroundUserService = LocalServices
+                        .getService(UserManagerInternal.class).isVisibleBackgroundFullUser(userid);
+            }
             mKey = Pair.create(component, userid);
         }
 
@@ -1937,19 +2116,28 @@
         }
 
         public boolean isSameUser(int userId) {
-            if (!isEnabledForCurrentProfiles()) {
+            if (!isEnabledForUser()) {
                 return false;
             }
             return userId == USER_ALL || userId == this.userid;
         }
 
         public boolean enabledAndUserMatches(int nid) {
-            if (!isEnabledForCurrentProfiles()) {
+            if (!isEnabledForUser()) {
                 return false;
             }
             if (this.userid == USER_ALL) return true;
             if (this.isSystem) return true;
             if (nid == USER_ALL || nid == this.userid) return true;
+            if (managedServicesConcurrentMultiuser()
+                    && mUmInternal.getProfileParentId(nid)
+                        != mUmInternal.getProfileParentId(this.userid)) {
+                // If the profile parent IDs do not match each other,
+                // it is determined that the users do not match.
+                // This situation may occur when comparing the current user's ID
+                // with the visible background user's ID.
+                return false;
+            }
             return supportsProfiles()
                     && mUserProfiles.isCurrentProfile(nid)
                     && isPermittedForProfile(nid);
@@ -1969,12 +2157,21 @@
             removeServiceImpl(this.service, this.userid);
         }
 
-        /** convenience method for looking in mEnabledServicesForCurrentProfiles */
-        public boolean isEnabledForCurrentProfiles() {
+        /**
+         * convenience method for looking in mEnabledServicesByUser.
+         * If FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER is disabled, this manages the data using
+         * only UserHandle.USER_CURRENT as the key, in order to behave the same as the legacy logic.
+        */
+        public boolean isEnabledForUser() {
             if (this.isSystem) return true;
             if (this.connection == null) return false;
             synchronized (mMutex) {
-                return mEnabledServicesForCurrentProfiles.contains(this.component);
+                int resolvedUserId = managedServicesConcurrentMultiuser()
+                        ? resolveUserId(this.userid)
+                        : UserHandle.USER_CURRENT;
+                ArraySet<ComponentName> enabledServices =
+                        mEnabledServicesByUser.get(resolvedUserId);
+                return enabledServices != null && enabledServices.contains(this.component);
             }
         }
 
@@ -2017,10 +2214,30 @@
         }
     }
 
-    /** convenience method for looking in mEnabledServicesForCurrentProfiles */
+    /** convenience method for looking in mEnabledServicesByUser for UserHandle.USER_CURRENT.
+     * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes
+     * trunk stable,  this API should be deprecated.  Additionally, when this method
+     * is deprecated, the unit tests written using this method should also be revised.
+     *
+     * @param component target component name
+     * @return boolean value that indicates whether it is enabled for the current profiles
+     */
     public boolean isComponentEnabledForCurrentProfiles(ComponentName component) {
+        return isComponentEnabledForUser(component, UserHandle.USER_CURRENT);
+    }
+
+    /** convenience method for looking in mEnabledServicesForUser
+     *
+     * @param component target component name
+     * @param userId the id of the target user
+     * @return boolean value that indicates whether it is enabled for the target user
+    */
+    @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public boolean isComponentEnabledForUser(ComponentName component, int userId) {
         synchronized (mMutex) {
-            return mEnabledServicesForCurrentProfiles.contains(component);
+            ArraySet<ComponentName> enabledServicesForUser =
+                    mEnabledServicesByUser.get(resolveUserId(userId));
+            return enabledServicesForUser != null && enabledServicesForUser.contains(component);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 340afb7..6fddfb5 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -173,6 +173,7 @@
 import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
 import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
 import static com.android.server.notification.Flags.expireBitmaps;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
 import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER;
 import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
 import static com.android.server.utils.PriorityDump.PRIORITY_ARG;
@@ -1207,7 +1208,7 @@
         }
 
         mAssistants.resetDefaultAssistantsIfNecessary();
-        mPreferencesHelper.syncChannelsBypassingDnd();
+        mPreferencesHelper.syncHasPriorityChannels();
     }
 
     @VisibleForTesting
@@ -2323,6 +2324,9 @@
                 if (userHandle >= 0) {
                     cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle,
                             REASON_USER_STOPPED);
+                    mConditionProviders.onUserStopped(userHandle);
+                    mListeners.onUserStopped(userHandle);
+                    mAssistants.onUserStopped(userHandle);
                 }
             } else if (
                     isProfileUnavailable(action)) {
@@ -2343,7 +2347,7 @@
                         mConditionProviders.onUserSwitched(userId);
                         mListeners.onUserSwitched(userId);
                         mZenModeHelper.onUserSwitched(userId);
-                        mPreferencesHelper.syncChannelsBypassingDnd();
+                        mPreferencesHelper.syncHasPriorityChannels();
                     }
                     // assistant is the only thing that cares about managed profiles specifically
                     mAssistants.onUserSwitched(userId);
@@ -2367,7 +2371,7 @@
                 mConditionProviders.onUserRemoved(userId);
                 mAssistants.onUserRemoved(userId);
                 mHistoryManager.onUserRemoved(userId);
-                mPreferencesHelper.syncChannelsBypassingDnd();
+                mPreferencesHelper.syncHasPriorityChannels();
                 handleSavePolicyFile();
             } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
@@ -2376,9 +2380,6 @@
                 if (!mUserProfiles.isProfileUser(userId, context)) {
                     mConditionProviders.onUserUnlocked(userId);
                     mListeners.onUserUnlocked(userId);
-                    if (!android.app.Flags.modesApi()) {
-                        mZenModeHelper.onUserUnlocked(userId);
-                    }
                 }
             }
         }
@@ -2767,9 +2768,7 @@
             void onPolicyChanged(Policy newPolicy) {
                 Binder.withCleanCallingIdentity(() -> {
                     Intent intent = new Intent(ACTION_NOTIFICATION_POLICY_CHANGED);
-                    if (android.app.Flags.modesApi()) {
-                        intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy);
-                    }
+                    intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy);
                     sendRegisteredOnlyBroadcast(intent);
                     mRankingHandler.requestSort();
                 });
@@ -2778,11 +2777,10 @@
             @Override
             void onConsolidatedPolicyChanged(Policy newConsolidatedPolicy) {
                 Binder.withCleanCallingIdentity(() -> {
-                    if (android.app.Flags.modesApi()) {
-                        Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED);
-                        intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy);
-                        sendRegisteredOnlyBroadcast(intent);
-                    }
+                    Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED);
+                    intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy);
+                    sendRegisteredOnlyBroadcast(intent);
+
                     mRankingHandler.requestSort();
                 });
             }
@@ -3368,7 +3366,7 @@
             migrateDefaultNAS();
             maybeShowInitialReviewPermissionsNotification();
 
-            if (android.app.Flags.modesApi() && !mZenModeHelper.hasDeviceEffectsApplier()) {
+            if (!mZenModeHelper.hasDeviceEffectsApplier()) {
                 // Cannot be done earlier, as some services aren't ready until this point.
                 mZenModeHelper.setDeviceEffectsApplier(
                         new DefaultDeviceEffectsApplier(getContext()));
@@ -3446,7 +3444,7 @@
             mConditionProviders.onUserSwitched(userId);
             mListeners.onUserSwitched(userId);
             mZenModeHelper.onUserSwitched(userId);
-            mPreferencesHelper.syncChannelsBypassingDnd();
+            mPreferencesHelper.syncHasPriorityChannels();
         }
         // assistant is the only thing that cares about managed profiles specifically
         mAssistants.onUserSwitched(userId);
@@ -5236,11 +5234,8 @@
 
         @Override
         public boolean areChannelsBypassingDnd() {
-            if (android.app.Flags.modesApi()) {
-                return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels()
-                        && mPreferencesHelper.areChannelsBypassingDnd();
-            }
-            return mPreferencesHelper.areChannelsBypassingDnd();
+            return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels()
+                    && mPreferencesHelper.hasPriorityChannels();
         }
 
         @Override
@@ -5730,12 +5725,13 @@
         public void requestBindListener(ComponentName component) {
             checkCallerIsSystemOrSameApp(component.getPackageName());
             int uid = Binder.getCallingUid();
+            int userId = UserHandle.getUserId(uid);
             final long identity = Binder.clearCallingIdentity();
             try {
-                ManagedServices manager =
-                        mAssistants.isComponentEnabledForCurrentProfiles(component)
-                        ? mAssistants
-                        : mListeners;
+                boolean isAssistantEnabled = managedServicesConcurrentMultiuser()
+                        ? mAssistants.isComponentEnabledForUser(component, userId)
+                        : mAssistants.isComponentEnabledForCurrentProfiles(component);
+                ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners;
                 manager.setComponentState(component, UserHandle.getUserId(uid), true);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -5762,16 +5758,16 @@
         public void requestUnbindListenerComponent(ComponentName component) {
             checkCallerIsSameApp(component.getPackageName());
             int uid = Binder.getCallingUid();
+            int userId = UserHandle.getUserId(uid);
             final long identity = Binder.clearCallingIdentity();
             try {
                 synchronized (mNotificationLock) {
-                    ManagedServices manager =
-                            mAssistants.isComponentEnabledForCurrentProfiles(component)
-                                    ? mAssistants
-                                    : mListeners;
-                    if (manager.isPackageOrComponentAllowed(component.flattenToString(),
-                            UserHandle.getUserId(uid))) {
-                        manager.setComponentState(component, UserHandle.getUserId(uid), false);
+                    boolean isAssistantEnabled = managedServicesConcurrentMultiuser()
+                            ? mAssistants.isComponentEnabledForUser(component, userId)
+                            : mAssistants.isComponentEnabledForCurrentProfiles(component);
+                    ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners;
+                    if (manager.isPackageOrComponentAllowed(component.flattenToString(), userId)) {
+                        manager.setComponentState(component, userId, false);
                     }
                 }
             } finally {
@@ -6092,43 +6088,27 @@
         @Override
         public void requestInterruptionFilterFromListener(INotificationListener token,
                 int interruptionFilter) throws RemoteException {
-            if (android.app.Flags.modesApi()) {
-                final int callingUid = Binder.getCallingUid();
-                ManagedServiceInfo info;
-                synchronized (mNotificationLock) {
-                    info = mListeners.checkServiceTokenLocked(token);
-                }
+            final int callingUid = Binder.getCallingUid();
+            ManagedServiceInfo info;
+            synchronized (mNotificationLock) {
+                info = mListeners.checkServiceTokenLocked(token);
+            }
 
-                final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1);
-                if (zenMode == -1) return;
+            final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1);
+            if (zenMode == -1) return;
 
-                UserHandle zenUser = getCallingZenUser();
-                if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) {
-                    mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(
-                            zenUser, info.component.getPackageName(), callingUid, zenMode);
-                } else {
-                    int origin = computeZenOrigin(/* fromUser= */ false);
-                    Binder.withCleanCallingIdentity(() -> {
-                        mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null,
-                                origin, "listener:" + info.component.flattenToShortString(),
-                                /* caller= */ info.component.getPackageName(),
-                                callingUid);
-                    });
-                }
+            UserHandle zenUser = getCallingZenUser();
+            if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) {
+                mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(
+                        zenUser, info.component.getPackageName(), callingUid, zenMode);
             } else {
-                final int callingUid = Binder.getCallingUid();
-                final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
-                final long identity = Binder.clearCallingIdentity();
-                try {
-                    synchronized (mNotificationLock) {
-                        final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
-                        mZenModeHelper.requestFromListener(info.component, interruptionFilter,
-                                callingUid, isSystemOrSystemUi);
-                        updateInterruptionFilterLocked();
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(identity);
-                }
+                int origin = computeZenOrigin(/* fromUser= */ false);
+                Binder.withCleanCallingIdentity(() -> {
+                    mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null,
+                            origin, "listener:" + info.component.flattenToShortString(),
+                            /* caller= */ info.component.getPackageName(),
+                            callingUid);
+                });
             }
         }
 
@@ -6177,19 +6157,8 @@
             }
         }
 
-        // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
-        @Override
-        public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException {
-            int callingUid = Binder.getCallingUid();
-            enforcePolicyAccess(callingUid, "getZenRules");
-            return mZenModeHelper.getZenRules(getCallingZenUser(), callingUid);
-        }
-
         @Override
         public Map<String, AutomaticZenRule> getAutomaticZenRules() {
-            if (!android.app.Flags.modesApi()) {
-                throw new IllegalStateException("getAutomaticZenRules called with flag off!");
-            }
             int callingUid = Binder.getCallingUid();
             enforcePolicyAccess(callingUid, "getAutomaticZenRules");
             return mZenModeHelper.getAutomaticZenRules(getCallingZenUser(), callingUid);
@@ -6260,50 +6229,40 @@
             // Implicit rules have no ConditionProvider or Activity. We allow the user to customize
             // them (via Settings), but not the owner app. Should the app want to start using it as
             // a "normal" rule, it must provide a CP/ConfigActivity too.
-            if (android.app.Flags.modesApi()) {
-                boolean isImplicitRuleUpdateFromSystem = updateId != null
-                        && ZenModeConfig.isImplicitRuleId(updateId)
-                        && isCallerSystemOrSystemUi();
-                if (!isImplicitRuleUpdateFromSystem
-                        && rule.getOwner() == null
-                        && rule.getConfigurationActivity() == null) {
-                    throw new NullPointerException(
-                            "Rule must have a ConditionProviderService and/or configuration "
-                                    + "activity");
-                }
-            } else {
-                if (rule.getOwner() == null && rule.getConfigurationActivity() == null) {
-                    throw new NullPointerException(
-                            "Rule must have a ConditionProviderService and/or configuration "
-                                    + "activity");
-                }
+            boolean isImplicitRuleUpdateFromSystem = updateId != null
+                    && ZenModeConfig.isImplicitRuleId(updateId)
+                    && isCallerSystemOrSystemUi();
+            if (!isImplicitRuleUpdateFromSystem
+                    && rule.getOwner() == null
+                    && rule.getConfigurationActivity() == null) {
+                throw new NullPointerException(
+                        "Rule must have a ConditionProviderService and/or configuration "
+                                + "activity");
             }
             Objects.requireNonNull(rule.getConditionId(), "ConditionId is null");
 
-            if (android.app.Flags.modesApi()) {
-                if (isCallerSystemOrSystemUi()) {
-                    return; // System callers can use any type.
-                }
-                int uid = Binder.getCallingUid();
-                int userId = UserHandle.getUserId(uid);
+            if (isCallerSystemOrSystemUi()) {
+                return; // System callers can use any type.
+            }
+            int uid = Binder.getCallingUid();
+            int userId = UserHandle.getUserId(uid);
 
-                if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) {
-                    boolean isDeviceOwner = Binder.withCleanCallingIdentity(
-                            () -> mDpm.isActiveDeviceOwner(uid));
-                    if (!isDeviceOwner) {
-                        throw new IllegalArgumentException(
-                                "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED");
-                    }
-                } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
-                    String wellbeingPackage = getContext().getResources().getString(
-                            com.android.internal.R.string.config_systemWellbeing);
-                    boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage)
-                            && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId);
-                    if (!isCallerWellbeing) {
-                        throw new IllegalArgumentException(
-                                "Only the 'Wellbeing' package can use AutomaticZenRules with "
-                                        + "TYPE_BEDTIME");
-                    }
+            if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) {
+                boolean isDeviceOwner = Binder.withCleanCallingIdentity(
+                        () -> mDpm.isActiveDeviceOwner(uid));
+                if (!isDeviceOwner) {
+                    throw new IllegalArgumentException(
+                            "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED");
+                }
+            } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
+                String wellbeingPackage = getContext().getResources().getString(
+                        com.android.internal.R.string.config_systemWellbeing);
+                boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage)
+                        && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId);
+                if (!isCallerWellbeing) {
+                    throw new IllegalArgumentException(
+                            "Only the 'Wellbeing' package can use AutomaticZenRules with "
+                                    + "TYPE_BEDTIME");
                 }
             }
         }
@@ -6386,9 +6345,7 @@
 
         @ZenModeConfig.ConfigOrigin
         private int computeZenOrigin(boolean fromUser) {
-            // "fromUser" is introduced with MODES_API, so only consider it in that case.
-            // (Non-MODES_API behavior should also not depend at all on ORIGIN_USER_IN_X).
-            if (android.app.Flags.modesApi() && fromUser) {
+            if (fromUser) {
                 if (isCallerSystemOrSystemUi()) {
                     return ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
                 } else {
@@ -6402,9 +6359,7 @@
         }
 
         private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) {
-            if (android.app.Flags.modesApi()
-                    && fromUser
-                    && !isCallerSystemOrSystemUiOrShell()) {
+            if (fromUser && !isCallerSystemOrSystemUiOrShell()) {
                 throw new SecurityException(TextUtils.formatSimple(
                         "Calling %s with fromUser == true is only allowed for system", method));
             }
@@ -6419,7 +6374,7 @@
             enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter");
             UserHandle zenUser = getCallingZenUser();
 
-            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+            if (!canManageGlobalZenPolicy(pkg, callingUid)) {
                 mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(zenUser, pkg, callingUid, zen);
                 return;
             }
@@ -6549,6 +6504,13 @@
             } catch (NameNotFoundException e) {
                 return false;
             }
+            if (managedServicesConcurrentMultiuser()) {
+                return checkPackagePolicyAccess(pkg)
+                        || mListeners.isComponentEnabledForPackage(pkg,
+                            UserHandle.getCallingUserId())
+                        || (mDpm != null
+                            && (mDpm.isActiveProfileOwner(uid) || mDpm.isActiveDeviceOwner(uid)));
+            }
             //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
             return checkPackagePolicyAccess(pkg)
                     || mListeners.isComponentEnabledForPackage(pkg)
@@ -6723,7 +6685,7 @@
         public Policy getNotificationPolicy(String pkg) {
             final int callingUid = Binder.getCallingUid();
             UserHandle zenUser = getCallingZenUser();
-            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+            if (!canManageGlobalZenPolicy(pkg, callingUid)) {
                 return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(zenUser, pkg);
             }
             final long identity = Binder.clearCallingIdentity();
@@ -6760,8 +6722,7 @@
             UserHandle zenUser = getCallingZenUser();
 
             boolean isSystemCaller = isCallerSystemOrSystemUiOrShell();
-            boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
-                    && !canManageGlobalZenPolicy(pkg, callingUid);
+            boolean shouldApplyAsImplicitRule = !canManageGlobalZenPolicy(pkg, callingUid);
 
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -6953,7 +6914,8 @@
                         android.Manifest.permission.INTERACT_ACROSS_USERS,
                         "setNotificationListenerAccessGrantedForUser for user " + userId);
             }
-            if (mUmInternal.isVisibleBackgroundFullUser(userId)) {
+            if (!managedServicesConcurrentMultiuser()
+                    && mUmInternal.isVisibleBackgroundFullUser(userId)) {
                 // The main use case for visible background users is the Automotive multi-display
                 // configuration where a passenger can use a secondary display while the driver is
                 // using the main display. NotificationListeners is designed only for the current
@@ -8219,9 +8181,6 @@
 
         @Override
         public void setDeviceEffectsApplier(DeviceEffectsApplier applier) {
-            if (!android.app.Flags.modesApi()) {
-                return;
-            }
             if (mZenModeHelper == null) {
                 throw new IllegalStateException("ZenModeHelper is not yet ready!");
             }
@@ -13165,7 +13124,8 @@
 
         @Override
         public void onUserUnlocked(int user) {
-            if (mUmInternal.isVisibleBackgroundFullUser(user)) {
+            if (!managedServicesConcurrentMultiuser()
+                    && mUmInternal.isVisibleBackgroundFullUser(user)) {
                 // The main use case for visible background users is the Automotive
                 // multi-display configuration where a passenger can use a secondary
                 // display while the driver is using the main display.
@@ -13805,7 +13765,7 @@
             // TODO (b/73052211): if the ranking update changed the notification type,
             // cancel notifications for NLSes that can't see them anymore
             for (final ManagedServiceInfo serviceInfo : getServices()) {
-                if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+                if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
                         serviceInfo, ActivityManager.getCurrentUser())) {
                     continue;
                 }
@@ -13833,7 +13793,7 @@
         @GuardedBy("mNotificationLock")
         public void notifyListenerHintsChangedLocked(final int hints) {
             for (final ManagedServiceInfo serviceInfo : getServices()) {
-                if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+                if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
                         serviceInfo, ActivityManager.getCurrentUser())) {
                     continue;
                 }
@@ -13889,7 +13849,7 @@
 
         public void notifyInterruptionFilterChanged(final int interruptionFilter) {
             for (final ManagedServiceInfo serviceInfo : getServices()) {
-                if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+                if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
                         serviceInfo, ActivityManager.getCurrentUser())) {
                     continue;
                 }
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
index 6c0035b..f680ce7 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
@@ -514,11 +514,14 @@
         final int fsi_state;
         final boolean is_locked;
         final int age_in_minutes;
+        final boolean is_promoted_ongoing;
+        final boolean has_promotable_characteristics;
         @DurationMillisLong long post_duration_millis; // Not final; calculated at the end.
 
         NotificationReported(NotificationRecordPair p,
                 NotificationReportedEvent eventType, int position, int buzzBeepBlink,
                 InstanceId groupId) {
+            final Notification notification = p.r.getSbn().getNotification();
             this.event_id = eventType.getId();
             this.uid = p.r.getUid();
             this.package_name = p.r.getSbn().getPackageName();
@@ -527,8 +530,8 @@
             this.channel_id_hash = p.getChannelIdHash();
             this.group_id_hash = p.getGroupIdHash();
             this.group_instance_id = (groupId == null) ? 0 : groupId.getId();
-            this.is_group_summary = p.r.getSbn().getNotification().isGroupSummary();
-            this.category = p.r.getSbn().getNotification().category;
+            this.is_group_summary = notification.isGroupSummary();
+            this.category = notification.category;
             this.style = p.getStyle();
             this.num_people = p.getNumPeople();
             this.position = position;
@@ -542,22 +545,18 @@
             this.assistant_ranking_score = p.r.getRankingScore();
             this.is_ongoing = p.r.getSbn().isOngoing();
             this.is_foreground_service = NotificationRecordLogger.isForegroundService(p.r);
-            this.timeout_millis = p.r.getSbn().getNotification().getTimeoutAfter();
+            this.timeout_millis = notification.getTimeoutAfter();
             this.is_non_dismissible = NotificationRecordLogger.isNonDismissible(p.r);
-
-            final boolean hasFullScreenIntent =
-                    p.r.getSbn().getNotification().fullScreenIntent != null;
-
-            final boolean hasFsiRequestedButDeniedFlag =  (p.r.getSbn().getNotification().flags
-                    & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0;
-
+            final boolean hasFullScreenIntent = notification.fullScreenIntent != null;
+            final boolean hasFsiRequestedButDeniedFlag =
+                (notification.flags & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0;
             this.fsi_state = NotificationRecordLogger.getFsiState(
                     hasFullScreenIntent, hasFsiRequestedButDeniedFlag, eventType);
-
             this.is_locked = p.r.isLocked();
-
             this.age_in_minutes = NotificationRecordLogger.getAgeInMinutes(
-                    p.r.getSbn().getPostTime(), p.r.getSbn().getNotification().getWhen());
+                    p.r.getSbn().getPostTime(), notification.getWhen());
+            this.is_promoted_ongoing = notification.isPromotedOngoing();
+            this.has_promotable_characteristics = notification.hasPromotableCharacteristics();
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
index fc0a776..e0e3fba 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
@@ -78,7 +78,9 @@
                 notificationReported.post_duration_millis,
                 notificationReported.fsi_state,
                 notificationReported.is_locked,
-                notificationReported.age_in_minutes);
+                notificationReported.age_in_minutes,
+                notificationReported.is_promoted_ongoing,
+                notificationReported.has_promotable_characteristics);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index c305d66..bc987ed 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -183,13 +183,8 @@
                             interruptionFilter = INTERRUPTION_FILTER_ALL;
                     }
                     final int filter = interruptionFilter;
-                    if (android.app.Flags.modesApi()) {
-                        mBinderService.setInterruptionFilter(callingPackage, filter,
-                                /* fromUser= */ true);
-                    } else {
-                        mBinderService.setInterruptionFilter(callingPackage, filter,
-                                /* fromUser= */ false);
-                    }
+                    mBinderService.setInterruptionFilter(callingPackage, filter,
+                            /* fromUser= */ true);
                 }
                 break;
                 case "allow_dnd": {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 3974c83..0fc182f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -233,11 +233,9 @@
     private SparseBooleanArray mLockScreenShowNotifications;
     private SparseBooleanArray mLockScreenPrivateNotifications;
     private boolean mIsMediaNotificationFilteringEnabled;
-    // When modes_api flag is enabled, this value only tracks whether the current user has any
-    // channels marked as "priority channels", but not necessarily whether they are permitted
-    // to bypass DND by current zen policy.
-    // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined.
-    private boolean mCurrentUserHasChannelsBypassingDnd;
+    // Whether the current user has any channels marked as "priority channels" -- but not
+    // necessarily whether they are permitted to bypass DND by current zen policy.
+    private boolean mCurrentUserHasPriorityChannels;
     private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
     private final boolean mShowReviewPermissionsNotification;
 
@@ -1063,7 +1061,7 @@
             r.groups.put(group.getId(), group);
         }
         if (needsDndChange) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
         }
         if (android.app.Flags.nmBinderPerfCacheChannels() && changed) {
             invalidateNotificationChannelGroupCache();
@@ -1150,7 +1148,7 @@
                         existing.setBypassDnd(bypassDnd);
                         needsPolicyFileChange = true;
 
-                        if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
+                        if (bypassDnd != mCurrentUserHasPriorityChannels
                                 || previousExistingImportance != existing.getImportance()) {
                             needsDndChange = true;
                         }
@@ -1214,7 +1212,7 @@
                 }
 
                 r.channels.put(channel.getId(), channel);
-                if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
+                if (channel.canBypassDnd() != mCurrentUserHasPriorityChannels) {
                     needsDndChange = true;
                 }
                 MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1224,7 +1222,7 @@
         }
 
         if (needsDndChange) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
         }
 
         if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
@@ -1317,14 +1315,14 @@
                 // relevantly affected without the parent channel already having been.
             }
 
-            if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
+            if (updatedChannel.canBypassDnd() != mCurrentUserHasPriorityChannels
                     || channel.getImportance() != updatedChannel.getImportance()) {
                 needsDndChange = true;
                 changed = true;
             }
         }
         if (needsDndChange) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
         }
         if (changed) {
             if (android.app.Flags.nmBinderPerfCacheChannels()) {
@@ -1550,7 +1548,7 @@
             }
         }
         if (channelBypassedDnd) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
         }
 
         if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
@@ -1745,7 +1743,7 @@
             }
         }
         if (groupBypassedDnd) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
         }
         if (android.app.Flags.nmBinderPerfCacheChannels()) {
             if (deletedChannels.size() > 0) {
@@ -1906,8 +1904,8 @@
             }
         }
         if (!deletedChannelIds.isEmpty()) {
-            if (mCurrentUserHasChannelsBypassingDnd) {
-                updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            if (mCurrentUserHasPriorityChannels) {
+                updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
             }
             if (android.app.Flags.nmBinderPerfCacheChannels()) {
                 invalidateNotificationChannelCache();
@@ -2098,7 +2096,7 @@
     }
 
     /**
-     * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+     * Syncs {@link #mCurrentUserHasPriorityChannels} with the current user's notification
      * policy before updating. Must be called:
      * <ul>
      *     <li>On system init, after channels and DND configurations are loaded.
@@ -2106,22 +2104,23 @@
      *     <li>If users are removed (the removed user could've been a profile of the current one).
      * </ul>
      */
-    void syncChannelsBypassingDnd() {
-        mCurrentUserHasChannelsBypassingDnd =
+    void syncHasPriorityChannels() {
+        mCurrentUserHasPriorityChannels =
                 (mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).state
-                        & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+                        & NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS) != 0;
 
-        updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+        updateCurrentUserHasPriorityChannels(/* callingUid= */ Process.SYSTEM_UID,
                 /* fromSystemOrSystemUi= */ true);
     }
 
     /**
      * Updates the user's NotificationPolicy based on whether the current userId has channels
-     * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
-     * when the current user (or its profiles) change.
+     * marked as "priority" (which might bypass DND, depending on the zen rule details). It should
+     * be called whenever a channel is created, updated, or deleted, or when the current user (or
+     * its profiles) change.
      */
     // TODO: b/368247671 - remove fromSystemOrSystemUi argument when modes_ui is inlined.
-    private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+    private void updateCurrentUserHasPriorityChannels(int callingUid,
             boolean fromSystemOrSystemUi) {
         ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
 
@@ -2149,13 +2148,13 @@
             }
         }
         boolean haveBypassingApps = candidatePkgs.size() > 0;
-        if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
-            mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+        if (mCurrentUserHasPriorityChannels != haveBypassingApps) {
+            mCurrentUserHasPriorityChannels = haveBypassingApps;
             if (android.app.Flags.modesUi()) {
                 mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT,
-                        mCurrentUserHasChannelsBypassingDnd);
+                        mCurrentUserHasPriorityChannels);
             } else {
-                updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid,
+                updateZenPolicy(mCurrentUserHasPriorityChannels, callingUid,
                         fromSystemOrSystemUi);
             }
         }
@@ -2188,16 +2187,20 @@
                         policy.priorityCategories, policy.priorityCallSenders,
                         policy.priorityMessageSenders, policy.suppressedVisualEffects,
                         (areChannelsBypassingDnd
-                                ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND : 0),
+                                ? NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS : 0),
                         policy.priorityConversationSenders),
                 fromSystemOrSystemUi ? ZenModeConfig.ORIGIN_SYSTEM
                         : ZenModeConfig.ORIGIN_APP,
                 callingUid);
     }
 
-    // TODO: b/310620812 - rename to hasPriorityChannels() when modes_api is inlined.
-    public boolean areChannelsBypassingDnd() {
-        return mCurrentUserHasChannelsBypassingDnd;
+    /**
+     * Whether the current user has any channels marked as "priority channels"
+     * ({@link NotificationChannel#canBypassDnd}), but not necessarily whether they are permitted
+     * to bypass the filters set by the current zen policy.
+     */
+    public boolean hasPriorityChannels() {
+        return mCurrentUserHasPriorityChannels;
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index fcc5e97..ec9a2db 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -16,7 +16,7 @@
 
 package com.android.server.notification;
 
-import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND;
+import static android.app.NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE;
 import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY;
@@ -285,11 +285,10 @@
                 return true;
             }
 
-            if (Flags.modesApi() && hasActiveRuleCountDiff()) {
-                // Rules with INTERRUPTION_FILTER_ALL were always possible but before MODES_API
-                // they were completely useless; now they can apply effects, so we want to log
-                // when they become active/inactive, even though DND itself (as in "notification
-                // blocking") is off.
+            if (hasActiveRuleCountDiff()) {
+                // Rules with INTERRUPTION_FILTER_ALL can apply effects, so we want to log when they
+                // become active/inactive, even though DND itself (as in "notification blocking")
+                // is off.
                 return true;
             }
 
@@ -331,7 +330,7 @@
                 }
             }
 
-            if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) {
+            if (mNewZenMode == ZEN_MODE_OFF) {
                 // If the mode is OFF -> OFF then there cannot be any *effective* change to policy.
                 // (Note that, in theory, a policy diff is impossible since we don't merge the
                 // policies of INTERRUPTION_FILTER_ALL rules; this is a "just in case" check).
@@ -439,24 +438,14 @@
 
         // Determine the number of (automatic & manual) rules active after the change takes place.
         int getNumRulesActive() {
-            if (!Flags.modesApi()) {
-                // If the zen mode has turned off, that means nothing can be active.
-                if (mNewZenMode == ZEN_MODE_OFF) {
-                    return 0;
-                }
-            }
             return numActiveRulesInConfig(mNewConfig);
         }
 
         /**
-         * Return a list of the types of each of the active rules in the configuration.
-         * Only available when {@code MODES_API} is active; otherwise returns an empty list.
+         * Return a list of the types of each of the active rules in the configuration (sorted by
+         * the numerical value of the type, and including duplicates).
          */
         int[] getActiveRuleTypes() {
-            if (!Flags.modesApi()) {
-                return new int[0];
-            }
-
             ArrayList<Integer> activeTypes = new ArrayList<>();
             List<ZenRule> activeRules = activeRulesList(mNewConfig);
             if (activeRules.size() == 0) {
@@ -476,77 +465,10 @@
             return out;
         }
 
-        /**
-         * Return our best guess as to whether the changes observed are due to a user action.
-         * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily
-         * distinguish between a system uid call indicating "user interacted with Settings" vs "a
-         * system app changed something automatically".
-         */
+        /** Return whether the changes observed are due to a user action. */
         boolean getIsUserAction() {
-            if (Flags.modesApi()) {
-                return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
-                        || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP;
-            }
-
-            // Approach for pre-MODES_API:
-            //   - if manual rule turned on or off, the calling UID is system, and the new manual
-            //     rule does not have an enabler set, guess that this is likely to be a user action.
-            //     This may represent a system app turning on DND automatically, but we guess "user"
-            //     in this case.
-            //         - note that this has a known failure mode of "manual rule turning off
-            //           automatically after the default time runs out". We currently have no way
-            //           of distinguishing this case from a user manually turning off the rule.
-            //         - the reason for checking the enabler field is that a call may look like it's
-            //           coming from a system UID, but if an enabler is set then the request came
-            //           from an external source. "enabler" will be blank when manual rule is turned
-            //           on from Quick Settings or Settings.
-            //   - if an automatic rule's state changes in whether it is "enabled", then
-            //     that is probably a user action.
-            //   - if an automatic rule goes from "not snoozing" to "snoozing", that is probably
-            //     a user action; that means that the user temporarily turned off DND associated
-            //     with that rule.
-            //   - if an automatic rule becomes active but does *not* change in its enabled state
-            //     (covered by a previous case anyway), we guess that this is an automatic change.
-            //   - if a rule is added or removed and the call comes from the system, we guess that
-            //     this is a user action (as system rules can't be added or removed without a user
-            //     action).
-            switch (getChangedRuleType()) {
-                case RULE_TYPE_MANUAL:
-                    // TODO(b/278888961): Distinguish the automatically-turned-off state
-                    return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null);
-                case RULE_TYPE_AUTOMATIC:
-                    for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) {
-                        if (d.wasAdded() || d.wasRemoved()) {
-                            // If the change comes from system, a rule being added/removed indicates
-                            // a likely user action. From an app, it's harder to know for sure.
-                            return isFromSystemOrSystemUi();
-                        }
-                        ZenModeDiff.FieldDiff enabled = d.getDiffForField(
-                                ZenModeDiff.RuleDiff.FIELD_ENABLED);
-                        if (enabled != null && enabled.hasDiff()) {
-                            return true;
-                        }
-                        ZenModeDiff.FieldDiff snoozing = d.getDiffForField(
-                                ZenModeDiff.RuleDiff.FIELD_SNOOZING);
-                        if (snoozing != null && snoozing.hasDiff() && (boolean) snoozing.to()) {
-                            return true;
-                        }
-                    }
-                    // If the change was in an automatic rule and none of the "probably triggered
-                    // by a user" cases apply, then it's probably an automatic change.
-                    return false;
-                case RULE_TYPE_UNKNOWN:
-                default:
-            }
-
-            // If the change wasn't in a rule, but was in the zen policy: consider to be user action
-            // if the calling uid is system
-            if (hasPolicyDiff() || hasChannelsBypassingDiff()) {
-                return mCallingUid == Process.SYSTEM_UID;
-            }
-
-            // don't know, or none of the other things triggered; assume not a user action
-            return false;
+            return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
+                    || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP;
         }
 
         boolean isFromSystemOrSystemUi() {
@@ -587,7 +509,7 @@
          */
         @Nullable
         byte[] getDNDPolicyProto() {
-            if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) {
+            if (mNewZenMode == ZEN_MODE_OFF) {
                 return null;
             }
 
@@ -628,13 +550,10 @@
                                 mNewPolicy.allowMessagesFrom()));
                 proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM,
                         mNewPolicy.allowConversationsFrom());
-
-                if (Flags.modesApi()) {
-                    proto.write(DNDPolicyProto.ALLOW_CHANNELS,
-                            mNewPolicy.allowPriorityChannels()
-                                    ? CHANNEL_POLICY_PRIORITY
-                                    : CHANNEL_POLICY_NONE);
-                }
+                proto.write(DNDPolicyProto.ALLOW_CHANNELS,
+                        mNewPolicy.allowPriorityChannels()
+                                ? CHANNEL_POLICY_PRIORITY
+                                : CHANNEL_POLICY_NONE);
             } else {
                 Log.wtf(TAG, "attempted to write zen mode log event with null policy");
             }
@@ -648,14 +567,14 @@
          */
         boolean getAreChannelsBypassing() {
             if (mNewPolicy != null) {
-                return (mNewPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0;
+                return (mNewPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0;
             }
             return false;
         }
 
         private boolean hasChannelsBypassingDiff() {
             boolean prevChannelsBypassing = mPrevPolicy != null
-                    ? (mPrevPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0 : false;
+                    ? (mPrevPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0 : false;
             return prevChannelsBypassing != getAreChannelsBypassing();
         }
 
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index bdca555..87ae781 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -19,7 +19,6 @@
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
 
-import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.ComponentName;
@@ -146,16 +145,12 @@
 
     // Returns whether the record is permitted to bypass DND when the zen mode is
     // ZEN_MODE_IMPORTANT_INTERRUPTIONS. This depends on whether the record's package priority is
-    // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and, if
-    // the modes_api flag is on, whether the given policy permits priority channels to bypass.
-    // TODO: b/310620812 - simplify when modes_api is inlined.
+    // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and whether the
+    // given policy permits priority channels to bypass.
     private boolean canRecordBypassDnd(NotificationRecord record,
             NotificationManager.Policy policy) {
         boolean inPriorityChannel = record.getPackagePriority() == Notification.PRIORITY_MAX;
-        if (Flags.modesApi()) {
-            return inPriorityChannel && policy.allowPriorityChannels();
-        }
-        return inPriorityChannel;
+        return inPriorityChannel && policy.allowPriorityChannels();
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index b39b6fd..889df51 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -157,6 +157,12 @@
     static final int RULE_LIMIT_PER_PACKAGE = 100;
     private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
 
+    /**
+     * Amount of time since last activation after which implicit rules that have never been
+     * customized by the user are automatically cleaned up.
+     */
+    private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30);
+
     private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;
 
     /**
@@ -326,9 +332,6 @@
      * applied immediately.
      */
     void setDeviceEffectsApplier(@NonNull DeviceEffectsApplier deviceEffectsApplier) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         synchronized (mConfigLock) {
             if (mDeviceEffectsApplier != null) {
                 throw new IllegalStateException("Already set up a DeviceEffectsApplier!");
@@ -350,11 +353,6 @@
         }
     }
 
-    // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
-    public void onUserUnlocked(int user) {
-        loadConfigForUser(user, "onUserUnlocked");
-    }
-
     void setPriorityOnlyDndExemptPackages(String[] packages) {
         mPriorityOnlyDndExemptPackages = packages;
     }
@@ -385,21 +383,6 @@
         return NotificationManager.zenModeToInterruptionFilter(mZenMode);
     }
 
-    // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
-    public void requestFromListener(ComponentName name, int filter, int callingUid,
-            boolean fromSystemOrSystemUi) {
-        final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
-        if (newZen != -1) {
-            // This change is known to be for UserHandle.CURRENT because NLSes for
-            // background users are unbound.
-            setManualZenMode(UserHandle.CURRENT, newZen, null,
-                    fromSystemOrSystemUi ? ORIGIN_SYSTEM : ORIGIN_APP,
-                    /* reason= */ "listener:" + (name != null ? name.flattenToShortString() : null),
-                    /* caller= */ name != null ? name.getPackageName() : null,
-                    callingUid);
-        }
-    }
-
     public void setSuppressedEffects(long suppressedEffects) {
         if (mSuppressedEffects == suppressedEffects) return;
         mSuppressedEffects = suppressedEffects;
@@ -414,33 +397,24 @@
         return mZenMode;
     }
 
-    // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined.
-    public List<ZenRule> getZenRules(UserHandle user, int callingUid) {
-        List<ZenRule> rules = new ArrayList<>();
-        synchronized (mConfigLock) {
-            ZenModeConfig config = getConfigLocked(user);
-            if (config == null) return rules;
-            for (ZenRule rule : config.automaticRules.values()) {
-                if (canManageAutomaticZenRule(rule, callingUid)) {
-                    rules.add(rule);
-                }
-            }
-        }
-        return rules;
-    }
-
     /**
      * Get the list of {@link AutomaticZenRule} instances that the calling package can manage
      * (which means the owned rules for a regular app, and every rule for system callers) together
      * with their ids.
      */
     Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) {
-        List<ZenRule> ruleList = getZenRules(user, callingUid);
-        HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size());
-        for (ZenRule rule : ruleList) {
-            rules.put(rule.id, zenRuleToAutomaticZenRule(rule));
+        HashMap<String, AutomaticZenRule> rules = new HashMap<>();
+        synchronized (mConfigLock) {
+            ZenModeConfig config = getConfigLocked(user);
+            if (config == null) return rules;
+
+            for (ZenRule rule : config.automaticRules.values()) {
+                if (canManageAutomaticZenRule(rule, callingUid)) {
+                    rules.put(rule.id, zenRuleToAutomaticZenRule(rule));
+                }
+            }
+            return rules;
         }
-        return rules;
     }
 
     public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id, int callingUid) {
@@ -511,9 +485,6 @@
     @GuardedBy("mConfigLock")
     private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, String pkg, ZenRule ruleToAdd,
             AutomaticZenRule azrToAdd, @ConfigOrigin int origin) {
-        if (!Flags.modesApi()) {
-            return ruleToAdd;
-        }
         String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd);
         if (deletedKey == null) {
             // Couldn't calculate the deletedRuleKey (condition or pkg null?). This should
@@ -561,9 +532,6 @@
      */
     private static void maybeReplaceDefaultRule(ZenModeConfig config, @Nullable ZenRule oldRule,
             AutomaticZenRule rule) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME
                 && (oldRule == null || oldRule.type != rule.getType())) {
             // Note: we must not verify canManageAutomaticZenRule here, since most likely they
@@ -572,7 +540,7 @@
                     ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
             if (sleepingRule != null
                     && !sleepingRule.enabled
-                    && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) {
+                    && !sleepingRule.isUserModified()) {
                 config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
             }
         }
@@ -599,18 +567,10 @@
             }
             ZenModeConfig newConfig = config.copy();
             ZenModeConfig.ZenRule newRule = requireNonNull(newConfig.automaticRules.get(ruleId));
-            if (!Flags.modesApi()) {
-                if (newRule.enabled != automaticZenRule.isEnabled()) {
-                    dispatchOnAutomaticRuleStatusChanged(config.user, newRule.getPkg(), ruleId,
-                            automaticZenRule.isEnabled()
-                                    ? AUTOMATIC_RULE_STATUS_ENABLED
-                                    : AUTOMATIC_RULE_STATUS_DISABLED);
-                }
-            }
 
             boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newConfig, newRule,
                     origin, /* isNew= */ false);
-            if (Flags.modesApi() && !updated) {
+            if (!updated) {
                 // Bail out so we don't have the side effects of updating a rule (i.e. dropping
                 // condition) when no changes happen.
                 return true;
@@ -643,10 +603,6 @@
      */
     void applyGlobalZenModeAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid,
             int zenMode) {
-        if (!android.app.Flags.modesApi()) {
-            Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!");
-            return;
-        }
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
             if (config == null) {
@@ -712,10 +668,6 @@
      */
     void applyGlobalPolicyAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid,
             NotificationManager.Policy policy) {
-        if (!android.app.Flags.modesApi()) {
-            Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
-            return;
-        }
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
             if (config == null) {
@@ -772,10 +724,6 @@
      */
     @Nullable
     Policy getNotificationPolicyFromImplicitZenRule(UserHandle user, String callingPkg) {
-        if (!android.app.Flags.modesApi()) {
-            Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!");
-            return getNotificationPolicy(user);
-        }
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
             if (config == null) {
@@ -814,7 +762,6 @@
                 .appendPath(pkg)
                 .build();
         rule.enabled = true;
-        rule.modified = false;
         rule.component = null;
         rule.configurationActivity = null;
         return rule;
@@ -918,15 +865,12 @@
 
     private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove,
             @ConfigOrigin int origin) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         // If an app deletes a previously customized rule, keep it around to preserve
         // the user's customization when/if it's recreated later.
         // We don't try to preserve system-owned rules because their conditionIds (used as
         // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot
         // delete a system-owned rule.
-        if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp()
+        if (origin == ORIGIN_APP && ruleToRemove.isUserModified()
                 && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
             String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
             if (deletedKey != null) {
@@ -952,7 +896,7 @@
             if (rule == null || !canManageAutomaticZenRule(rule, callingUid)) {
                 return Condition.STATE_UNKNOWN;
             }
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 return rule.isActive() ? STATE_TRUE : STATE_FALSE;
             } else {
                 // Buggy, does not consider snoozing!
@@ -971,16 +915,9 @@
 
             newConfig = config.copy();
             ZenRule rule = newConfig.automaticRules.get(id);
-            if (Flags.modesApi()) {
-                if (rule != null && canManageAutomaticZenRule(rule, callingUid)) {
-                    setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule),
-                            condition, origin, "setAzrState: " + rule.id, callingUid);
-                }
-            } else {
-                ArrayList<ZenRule> rules = new ArrayList<>();
-                rules.add(rule); // rule may be null and throw NPE in the next method.
-                setAutomaticZenRuleStateLocked(newConfig, rules, condition, origin,
-                        "setAzrState: " + (rule != null ? rule.id : "null!"), callingUid);
+            if (rule != null && canManageAutomaticZenRule(rule, callingUid)) {
+                setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule),
+                        condition, origin, "setAzrState: " + rule.id, callingUid);
             }
         }
     }
@@ -995,13 +932,12 @@
             newConfig = config.copy();
 
             List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleConditionId, condition);
-            if (Flags.modesApi()) {
-                for (int i = matchingRules.size() - 1; i >= 0; i--) {
-                    if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) {
-                        matchingRules.remove(i);
-                    }
+            for (int i = matchingRules.size() - 1; i >= 0; i--) {
+                if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) {
+                    matchingRules.remove(i);
                 }
             }
+
             setAutomaticZenRuleStateLocked(newConfig, matchingRules, condition, origin,
                     "setAzrStateFromCps: " + ruleConditionId, callingUid);
         }
@@ -1013,7 +949,7 @@
         if (rules == null || rules.isEmpty()) return;
 
         if (!Flags.modesUi()) {
-            if (Flags.modesApi() && condition.source == SOURCE_USER_ACTION) {
+            if (condition.source == SOURCE_USER_ACTION) {
                 origin = ORIGIN_USER_IN_APP; // Although coming from app, it's actually from user.
             }
         }
@@ -1026,7 +962,7 @@
 
     private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition,
             int origin) {
-        if (Flags.modesApi() && Flags.modesUi()) {
+        if (Flags.modesUi()) {
             if (isImplicitRuleId(rule.id)) {
                 // Implicit rules do not use overrides, and always apply conditions directly.
                 // This is compatible with the previous behavior (where the package set the
@@ -1173,8 +1109,7 @@
                 // if default rule wasn't user-modified use localized name
                 // instead of previous system name
                 if (currRule != null
-                        && !currRule.modified
-                        && (currRule.zenPolicyUserModifiedFields & AutomaticZenRule.FIELD_NAME) == 0
+                        && (currRule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0
                         && !defaultRule.name.equals(currRule.name)) {
                     if (DEBUG) {
                         Slog.d(TAG, "Locale change - updating default zen rule name "
@@ -1184,7 +1119,7 @@
                     updated = true;
                 }
             }
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 for (ZenRule rule : newConfig.automaticRules.values()) {
                     if (SystemZenRules.isSystemOwnedRule(rule)) {
                         updated |= SystemZenRules.updateTriggerDescription(mContext, rule);
@@ -1256,172 +1191,145 @@
     @GuardedBy("mConfigLock")
     private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenModeConfig config,
             ZenRule rule, @ConfigOrigin int origin, boolean isNew) {
-        if (Flags.modesApi()) {
-            boolean modified = false;
-            // These values can always be edited by the app, so we apply changes immediately.
-            if (isNew) {
-                rule.id = ZenModeConfig.newRuleId();
-                rule.creationTime = mClock.millis();
-                rule.component = azr.getOwner();
-                rule.pkg = pkg;
-                modified = true;
-            }
 
-            // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule)
-            if (Flags.modesUi()
-                    && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI)
-                    && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID)
-                    && !Objects.equals(rule.component, azr.getOwner())) {
-                rule.component = azr.getOwner();
-                modified = true;
-            }
-
-            if (Flags.modesUi()) {
-                if (!azr.isEnabled() && (isNew || rule.enabled)) {
-                    // Creating a rule as disabled, or disabling a previously enabled rule.
-                    // Record whodunit.
-                    rule.disabledOrigin = origin;
-                } else if (azr.isEnabled()) {
-                    // Enabling or previously enabled. Clear disabler.
-                    rule.disabledOrigin = ORIGIN_UNKNOWN;
-                }
-            }
-
-            if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
-                rule.conditionId = azr.getConditionId();
-                modified = true;
-            }
-            // This can be removed when {@link Flags#modesUi} is fully ramped up
-            final boolean isWatch =
-                    mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
-            boolean shouldPreserveCondition =
-                    Flags.modesApi()
-                            && (Flags.modesUi() || isWatch)
-                            && !isNew
-                            && origin == ORIGIN_USER_IN_SYSTEMUI
-                            && rule.enabled == azr.isEnabled()
-                            && rule.conditionId != null
-                            && rule.condition != null
-                            && rule.conditionId.equals(rule.condition.id);
-            if (!shouldPreserveCondition) {
-                // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR.
-                rule.condition = null;
-            }
-
-            if (rule.enabled != azr.isEnabled()) {
-                rule.enabled = azr.isEnabled();
-                rule.resetConditionOverride();
-                modified = true;
-            }
-            if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
-                rule.configurationActivity = azr.getConfigurationActivity();
-                modified = true;
-            }
-            if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) {
-                rule.allowManualInvocation = azr.isManualInvocationAllowed();
-                modified = true;
-            }
-            if (!Flags.modesUi()) {
-                String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
-                if (!Objects.equals(rule.iconResName, iconResName)) {
-                    rule.iconResName = iconResName;
-                    modified = true;
-                }
-            }
-            if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) {
-                rule.triggerDescription = azr.getTriggerDescription();
-                modified = true;
-            }
-            if (rule.type != azr.getType()) {
-                rule.type = azr.getType();
-                modified = true;
-            }
-            // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
-            rule.modified = azr.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 = azr.getName();
-                modified |= !Objects.equals(rule.name, previousName);
-            }
-
-            // 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 modified;
-            }
-
-            // Updates the bitmasks if the origin of the change is the user.
-            boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI);
-
-            if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) {
-                rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
-            }
-            int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
-                    azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-            if (rule.zenMode != newZenMode) {
-                rule.zenMode = newZenMode;
-                if (updateBitmask) {
-                    rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
-                }
-                modified = true;
-            }
-
-            if (Flags.modesUi()) {
-                String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
-                if (!Objects.equals(rule.iconResName, iconResName)) {
-                    rule.iconResName = iconResName;
-                    if (updateBitmask) {
-                        rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON;
-                    }
-                    modified = true;
-                }
-            }
-
-            // Updates the bitmask and values for all policy fields, based on the origin.
-            modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew);
-
-            // Updates the bitmask and values for all device effect fields, based on the origin.
-            modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(),
-                    origin == ORIGIN_APP, updateBitmask);
-
-            return modified;
-        } else {
-            if (rule.enabled != azr.isEnabled()) {
-                rule.resetConditionOverride();
-            }
-            rule.name = azr.getName();
-            rule.condition = null;
-            rule.conditionId = azr.getConditionId();
-            rule.enabled = azr.isEnabled();
-            rule.modified = azr.isModified();
-            rule.zenPolicy = azr.getZenPolicy();
-            rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
-                    azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-            rule.configurationActivity = azr.getConfigurationActivity();
-
-            if (isNew) {
-                rule.id = ZenModeConfig.newRuleId();
-                rule.creationTime = System.currentTimeMillis();
-                rule.component = azr.getOwner();
-                rule.pkg = pkg;
-            }
-
-            // Only the MODES_API path cares about the result, so just return whatever here.
-            return true;
+        boolean modified = false;
+        // These values can always be edited by the app, so we apply changes immediately.
+        if (isNew) {
+            rule.id = ZenModeConfig.newRuleId();
+            rule.creationTime = mClock.millis();
+            rule.component = azr.getOwner();
+            rule.pkg = pkg;
+            modified = true;
         }
+
+        // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule)
+        if (Flags.modesUi()
+                && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI)
+                && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID)
+                && !Objects.equals(rule.component, azr.getOwner())) {
+            rule.component = azr.getOwner();
+            modified = true;
+        }
+
+        if (Flags.modesUi()) {
+            if (!azr.isEnabled() && (isNew || rule.enabled)) {
+                // Creating a rule as disabled, or disabling a previously enabled rule.
+                // Record whodunit.
+                rule.disabledOrigin = origin;
+            } else if (azr.isEnabled()) {
+                // Enabling or previously enabled. Clear disabler.
+                rule.disabledOrigin = ORIGIN_UNKNOWN;
+            }
+        }
+
+        if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
+            rule.conditionId = azr.getConditionId();
+            modified = true;
+        }
+        // This can be removed when {@link Flags#modesUi} is fully ramped up
+        final boolean isWatch =
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+        boolean shouldPreserveCondition =
+                (Flags.modesUi() || isWatch)
+                        && !isNew
+                        && origin == ORIGIN_USER_IN_SYSTEMUI
+                        && rule.enabled == azr.isEnabled()
+                        && rule.conditionId != null
+                        && rule.condition != null
+                        && rule.conditionId.equals(rule.condition.id);
+        if (!shouldPreserveCondition) {
+            // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR.
+            rule.condition = null;
+        }
+
+        if (rule.enabled != azr.isEnabled()) {
+            rule.enabled = azr.isEnabled();
+            rule.resetConditionOverride();
+            modified = true;
+        }
+        if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
+            rule.configurationActivity = azr.getConfigurationActivity();
+            modified = true;
+        }
+        if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) {
+            rule.allowManualInvocation = azr.isManualInvocationAllowed();
+            modified = true;
+        }
+        if (!Flags.modesUi()) {
+            String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
+            if (!Objects.equals(rule.iconResName, iconResName)) {
+                rule.iconResName = iconResName;
+                modified = true;
+            }
+        }
+        if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) {
+            rule.triggerDescription = azr.getTriggerDescription();
+            modified = true;
+        }
+        if (rule.type != azr.getType()) {
+            rule.type = azr.getType();
+            modified = true;
+        }
+
+        // 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 = azr.getName();
+            modified |= !Objects.equals(rule.name, previousName);
+        }
+
+        // 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.isUserModified();
+
+        // For all other values, if updates are not allowed, we discard the update.
+        if (!updateValues) {
+            return modified;
+        }
+
+        // Updates the bitmasks if the origin of the change is the user.
+        boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI);
+
+        if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) {
+            rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+        }
+        int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
+                azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+        if (rule.zenMode != newZenMode) {
+            rule.zenMode = newZenMode;
+            if (updateBitmask) {
+                rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+            }
+            modified = true;
+        }
+
+        if (Flags.modesUi()) {
+            String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
+            if (!Objects.equals(rule.iconResName, iconResName)) {
+                rule.iconResName = iconResName;
+                if (updateBitmask) {
+                    rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON;
+                }
+                modified = true;
+            }
+        }
+
+        // Updates the bitmask and values for all policy fields, based on the origin.
+        modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew);
+
+        // Updates the bitmask and values for all device effect fields, based on the origin.
+        modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(),
+                origin == ORIGIN_APP, updateBitmask);
+
+        return modified;
     }
 
     /**
@@ -1629,32 +1537,21 @@
     }
 
     private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
-        AutomaticZenRule azr;
-        if (Flags.modesApi()) {
-            azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
-                    .setManualInvocationAllowed(rule.allowManualInvocation)
-                    .setPackage(rule.pkg)
-                    .setCreationTime(rule.creationTime)
-                    .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName))
-                    .setType(rule.type)
-                    .setZenPolicy(rule.zenPolicy)
-                    .setDeviceEffects(rule.zenDeviceEffects)
-                    .setEnabled(rule.enabled)
-                    .setInterruptionFilter(
-                            NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
-                    .setOwner(rule.component)
-                    .setConfigurationActivity(rule.configurationActivity)
-                    .setTriggerDescription(rule.triggerDescription)
-                    .build();
-        } else {
-            azr = new AutomaticZenRule(rule.name, rule.component,
-                    rule.configurationActivity,
-                    rule.conditionId, rule.zenPolicy,
-                    NotificationManager.zenModeToInterruptionFilter(rule.zenMode),
-                    rule.enabled, rule.creationTime);
-            azr.setPackageName(rule.pkg);
-        }
-        return azr;
+        return new AutomaticZenRule.Builder(rule.name, rule.conditionId)
+                .setManualInvocationAllowed(rule.allowManualInvocation)
+                .setPackage(rule.pkg)
+                .setCreationTime(rule.creationTime)
+                .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName))
+                .setType(rule.type)
+                .setZenPolicy(rule.zenPolicy)
+                .setDeviceEffects(rule.zenDeviceEffects)
+                .setEnabled(rule.enabled)
+                .setInterruptionFilter(
+                        NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
+                .setOwner(rule.component)
+                .setConfigurationActivity(rule.configurationActivity)
+                .setTriggerDescription(rule.triggerDescription)
+                .build();
     }
 
     // Update only the hasPriorityChannels state (aka areChannelsBypassingDnd) without modifying
@@ -1669,12 +1566,12 @@
             if (config == null) return;
 
             // If it already matches, do nothing
-            if (config.areChannelsBypassingDnd == hasPriorityChannels) {
+            if (config.hasPriorityChannels == hasPriorityChannels) {
                 return;
             }
 
             ZenModeConfig newConfig = config.copy();
-            newConfig.areChannelsBypassingDnd = hasPriorityChannels;
+            newConfig.hasPriorityChannels = hasPriorityChannels;
             // The updated calculation of whether there are priority channels is always done by
             // the system, even if the event causing the calculation had a different origin.
             setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "updateHasPriorityChannels",
@@ -1754,9 +1651,7 @@
                     newRule.zenMode = zenMode;
                     newRule.conditionId = conditionId;
                     newRule.enabler = caller;
-                    if (Flags.modesApi()) {
-                        newRule.allowManualInvocation = true;
-                    }
+                    newRule.allowManualInvocation = true;
                     newConfig.manualRule = newRule;
                 }
             }
@@ -1849,7 +1744,7 @@
             boolean hasDefaultRules = config.automaticRules.containsAll(
                     ZenModeConfig.getDefaultRuleIds());
 
-            long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+            long time = mClock.millis();
             if (config.automaticRules != null && config.automaticRules.size() > 0) {
                 for (ZenRule automaticRule : config.automaticRules.values()) {
                     if (forRestore) {
@@ -1863,7 +1758,7 @@
 
                     // Upon upgrading to a version with modes_api enabled, keep all behaviors of
                     // rules with null ZenPolicies explicitly as a copy of the global policy.
-                    if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) {
+                    if (config.version < ZenModeConfig.XML_VERSION_MODES_API) {
                         // Keep the manual ("global") policy that from config.
                         ZenPolicy manualRulePolicy = config.getZenPolicy();
                         if (automaticRule.zenPolicy == null) {
@@ -1877,8 +1772,7 @@
                         }
                     }
 
-                    if (Flags.modesApi() && Flags.modesUi()
-                            && config.version < ZenModeConfig.XML_VERSION_MODES_UI) {
+                    if (Flags.modesUi() && config.version < ZenModeConfig.XML_VERSION_MODES_UI) {
                         // Clear icons from implicit rules. App icons are not suitable for some
                         // surfaces, so juse use a default (the user can select a different one).
                         if (ZenModeConfig.isImplicitRuleId(automaticRule.id)) {
@@ -1904,11 +1798,11 @@
                 reason += ", reset to default rules";
             }
 
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 SystemZenRules.maybeUpgradeRules(mContext, config);
             }
 
-            if (Flags.modesApi() && forRestore) {
+            if (forRestore) {
                 // Note: forBackup doesn't write deletedRules, but just in case.
                 config.deletedRules.clear();
             }
@@ -1995,7 +1889,7 @@
             if (config == null) return;
 
             final ZenModeConfig newConfig = config.copy();
-            if (Flags.modesApi() && !Flags.modesUi()) {
+            if (!Flags.modesUi()) {
                 // Fix for b/337193321 -- propagate changes to notificationPolicy to rules where
                 // the user cannot edit zen policy to emulate the previous "inheritance".
                 ZenPolicy previousPolicy = ZenAdapters.notificationPolicyToZenPolicy(
@@ -2026,6 +1920,7 @@
      * <ul>
      *     <li>Rule instances whose owner is not installed.
      *     <li>Deleted rules that were deleted more than 30 days ago.
+     *     <li>Implicit rules that haven't been used in 30 days (and have not been customized).
      * </ul>
      */
     private void cleanUpZenRules() {
@@ -2034,17 +1929,20 @@
             final ZenModeConfig newConfig = mConfig.copy();
 
             deleteRulesWithoutOwner(newConfig.automaticRules);
-            if (Flags.modesApi()) {
-                deleteRulesWithoutOwner(newConfig.deletedRules);
-                for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
-                    ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
-                    if (deletedRule.deletionInstant == null
-                            || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
-                        newConfig.deletedRules.removeAt(i);
-                    }
+            deleteRulesWithoutOwner(newConfig.deletedRules);
+
+            for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+                ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
+                if (deletedRule.deletionInstant == null
+                        || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
+                    newConfig.deletedRules.removeAt(i);
                 }
             }
 
+            if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
+                deleteUnusedImplicitRules(newConfig.automaticRules);
+            }
+
             if (!newConfig.equals(mConfig)) {
                 setConfigLocked(newConfig, null, ORIGIN_SYSTEM,
                         "cleanUpZenRules", Process.SYSTEM_UID);
@@ -2053,7 +1951,7 @@
     }
 
     private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) {
-        long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+        long currentTime = mClock.millis();
         if (ruleList != null) {
             for (int i = ruleList.size() - 1; i >= 0; i--) {
                 ZenRule rule = ruleList.valueAt(i);
@@ -2070,6 +1968,29 @@
         }
     }
 
+    private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) {
+        if (ruleList == null) {
+            return;
+        }
+        Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR);
+
+        for (int i = ruleList.size() - 1; i >= 0; i--) {
+            ZenRule rule = ruleList.valueAt(i);
+            if (isImplicitRuleId(rule.id) && !rule.isUserModified()) {
+                if (rule.lastActivation == null) {
+                    // This rule existed before we started tracking activation time. It *might* be
+                    // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR)
+                    // before being removed if truly unused.
+                    rule.lastActivation = mClock.instant();
+                }
+
+                if (rule.lastActivation.isBefore(deleteIfUnusedSince)) {
+                    ruleList.removeAt(i);
+                }
+            }
+        }
+    }
+
     /**
      * @return a copy of the zen mode configuration
      */
@@ -2188,7 +2109,7 @@
                 mZenMode, mConfig, mConsolidatedPolicy);
         if (!config.equals(mConfig)) {
             // Schedule broadcasts. Cannot be sent during boot, though.
-            if (Flags.modesApi() && origin != ORIGIN_INIT) {
+            if (origin != ORIGIN_INIT) {
                 for (ZenRule rule : config.automaticRules.values()) {
                     ZenRule original = mConfig.automaticRules.get(rule.id);
                     if (original != null) {
@@ -2204,6 +2125,20 @@
                 }
             }
 
+            // Update last activation for rules that are being activated.
+            if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
+                Instant now = mClock.instant();
+                if (!mConfig.isManualActive() && config.isManualActive()) {
+                    config.manualRule.lastActivation = now;
+                }
+                for (ZenRule rule : config.automaticRules.values()) {
+                    ZenRule previousRule = mConfig.automaticRules.get(rule.id);
+                    if (rule.isActive() && (previousRule == null || !previousRule.isActive())) {
+                        rule.lastActivation = now;
+                    }
+                }
+            }
+
             mConfig = config;
             dispatchOnConfigChanged();
             updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason);
@@ -2295,7 +2230,7 @@
     private void applyCustomPolicy(ZenModeConfig config, ZenPolicy policy, ZenRule rule,
             boolean useManualConfig) {
         if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 policy.apply(ZenPolicy.getBasePolicyInterruptionFilterNone());
             } else {
                 policy.apply(new ZenPolicy.Builder()
@@ -2304,7 +2239,7 @@
                         .build());
             }
         } else if (rule.zenMode == Global.ZEN_MODE_ALARMS) {
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 policy.apply(ZenPolicy.getBasePolicyInterruptionFilterAlarms());
             } else {
                 policy.apply(new ZenPolicy.Builder()
@@ -2317,22 +2252,17 @@
         } else if (rule.zenPolicy != null) {
             policy.apply(rule.zenPolicy);
         } else {
-            if (Flags.modesApi()) {
-                if (useManualConfig) {
-                    // manual rule is configured using the settings stored directly in ZenModeConfig
-                    policy.apply(config.getZenPolicy());
-                } else {
-                    // under modes_api flag, an active automatic rule with no specified policy
-                    // inherits the device default settings as stored in mDefaultConfig. While the
-                    // rule's policy fields should be set upon creation, this is a fallback to
-                    // catch any that may have fallen through the cracks.
-                    Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
-                    policy.apply(Flags.modesUi()
-                            ? mDefaultConfig.getZenPolicy() : config.getZenPolicy());
-                }
-            } else {
-                // active rule with no specified policy inherits the manual rule config settings
+            if (useManualConfig) {
+                // manual rule is configured using the settings stored directly in ZenModeConfig
                 policy.apply(config.getZenPolicy());
+            } else {
+                // An active automatic rule with no specified policy inherits the device default
+                // settings as stored in mDefaultConfig. While the rule's policy fields should be
+                // set upon creation, this is a fallback to catch any that may have fallen through
+                // the cracks.
+                Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
+                policy.apply(Flags.modesUi()
+                        ? mDefaultConfig.getZenPolicy() : config.getZenPolicy());
             }
         }
     }
@@ -2346,9 +2276,7 @@
             ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder();
             if (mConfig.isManualActive()) {
                 applyCustomPolicy(mConfig, policy, mConfig.manualRule, true);
-                if (Flags.modesApi()) {
-                    deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
-                }
+                deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
             }
 
             for (ZenRule automaticRule : mConfig.automaticRules.values()) {
@@ -2356,12 +2284,10 @@
                     // Active rules with INTERRUPTION_FILTER_ALL are not included in consolidated
                     // policy. This is relevant in case some other active rule has a more
                     // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy!
-                    if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) {
+                    if (automaticRule.zenMode != Global.ZEN_MODE_OFF) {
                         applyCustomPolicy(mConfig, policy, automaticRule, false);
                     }
-                    if (Flags.modesApi()) {
-                        deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
-                    }
+                    deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
                 }
             }
 
@@ -2380,40 +2306,35 @@
                 ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason);
             }
 
-            if (Flags.modesApi()) {
-                // Prevent other rules from applying grayscale if Driving is active (but allow it
-                // if _Driving itself_ wants grayscale).
-                if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) {
-                    boolean hasActiveDriving = false;
-                    boolean hasActiveDrivingWithGrayscale = false;
-                    for (ZenRule rule : mConfig.automaticRules.values()) {
-                        if (rule.isActive() && rule.type == TYPE_DRIVING) {
-                            hasActiveDriving = true;
-                            if (rule.zenDeviceEffects != null
-                                    && rule.zenDeviceEffects.shouldDisplayGrayscale()) {
-                                hasActiveDrivingWithGrayscale = true;
-                                break; // Further rules won't affect decision.
-                            }
+            // Prevent other rules from applying grayscale if Driving is active (but allow it
+            // if _Driving itself_ wants grayscale).
+            if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) {
+                boolean hasActiveDriving = false;
+                boolean hasActiveDrivingWithGrayscale = false;
+                for (ZenRule rule : mConfig.automaticRules.values()) {
+                    if (rule.isActive() && rule.type == TYPE_DRIVING) {
+                        hasActiveDriving = true;
+                        if (rule.zenDeviceEffects != null
+                                && rule.zenDeviceEffects.shouldDisplayGrayscale()) {
+                            hasActiveDrivingWithGrayscale = true;
+                            break; // Further rules won't affect decision.
                         }
                     }
-                    if (hasActiveDriving && !hasActiveDrivingWithGrayscale) {
-                        deviceEffectsBuilder.setShouldDisplayGrayscale(false);
-                    }
                 }
+                if (hasActiveDriving && !hasActiveDrivingWithGrayscale) {
+                    deviceEffectsBuilder.setShouldDisplayGrayscale(false);
+                }
+            }
 
-                ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
-                if (!deviceEffects.equals(mConsolidatedDeviceEffects)) {
-                    mConsolidatedDeviceEffects = deviceEffects;
-                    mHandler.postApplyDeviceEffects(origin);
-                }
+            ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+            if (!deviceEffects.equals(mConsolidatedDeviceEffects)) {
+                mConsolidatedDeviceEffects = deviceEffects;
+                mHandler.postApplyDeviceEffects(origin);
             }
         }
     }
 
     private void applyConsolidatedDeviceEffects(@ConfigOrigin int source) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         DeviceEffectsApplier applier;
         ZenDeviceEffects effects;
         synchronized (mConfigLock) {
@@ -2434,10 +2355,8 @@
      * to the current locale.
      */
     private static void updateDefaultConfig(Context context, ZenModeConfig defaultConfig) {
-        if (Flags.modesApi()) {
-            updateDefaultAutomaticRulePolicies(defaultConfig);
-        }
-        if (Flags.modesApi() && Flags.modesUi()) {
+        updateDefaultAutomaticRulePolicies(defaultConfig);
+        if (Flags.modesUi()) {
             SystemZenRules.maybeUpgradeRules(context, defaultConfig);
         }
         updateRuleStringsForCurrentLocale(context, defaultConfig);
@@ -2453,7 +2372,7 @@
                 rule.name = context.getResources()
                         .getString(R.string.zen_mode_default_every_night_name);
             }
-            if (Flags.modesApi() && Flags.modesUi()) {
+            if (Flags.modesUi()) {
                 SystemZenRules.updateTriggerDescription(context, rule);
             }
         }
@@ -2462,10 +2381,6 @@
     // Updates the policies in the default automatic rules (provided via default XML config) to
     // be fully filled in default values.
     private static void updateDefaultAutomaticRulePolicies(ZenModeConfig defaultConfig) {
-        if (!Flags.modesApi()) {
-            // Should be checked before calling, but just in case.
-            return;
-        }
         ZenPolicy defaultPolicy = defaultConfig.getZenPolicy();
         for (ZenRule rule : defaultConfig.automaticRules.values()) {
             if (ZenModeConfig.getDefaultRuleIds().contains(rule.id) && rule.zenPolicy == null) {
@@ -2611,6 +2526,7 @@
         }
     }
 
+    // TODO: b/368247671 - Delete this method AND default_zen_mode_config.xml when inlining modes_ui
     private ZenModeConfig readDefaultConfig(Resources resources) {
         XmlResourceParser parser = null;
         try {
@@ -2649,7 +2565,7 @@
                 events.add(FrameworkStatsLog.buildStatsEvent(DND_MODE_RULE,
                         /* optional int32 user = 1 */ user,
                         /* optional bool enabled = 2 */ config.isManualActive(),
-                        /* optional bool channels_bypassing = 3 */ config.areChannelsBypassingDnd,
+                        /* optional bool channels_bypassing = 3 */ config.hasPriorityChannels,
                         /* optional LoggedZenMode zen_mode = 4 */ ROOT_CONFIG,
                         /* optional string id = 5 */ "", // empty for root config
                         /* optional int32 uid = 6 */ Process.SYSTEM_UID, // system owns root config
@@ -2924,9 +2840,6 @@
      * ({@link #addAutomaticZenRule}, {@link #removeAutomaticZenRule}, etc, makes sense.
      */
     private static void checkManageRuleOrigin(String method, @ConfigOrigin int origin) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         checkArgument(origin == ORIGIN_APP || origin == ORIGIN_SYSTEM
                         || origin == ORIGIN_USER_IN_SYSTEMUI,
                 "Expected one of ORIGIN_APP, ORIGIN_SYSTEM, or "
@@ -2939,9 +2852,6 @@
      * {@link #setAutomaticZenRuleStateFromConditionProvider} makes sense.
      */
     private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) {
-        if (!Flags.modesApi()) {
-            return;
-        }
         checkArgument(origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP
                         || origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI,
                 "Expected one of ORIGIN_APP, ORIGIN_USER_IN_APP, ORIGIN_SYSTEM, or "
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 048f2b6..76cd5c8 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -210,3 +210,10 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "managed_services_concurrent_multiuser"
+  namespace: "systemui"
+  description: "Enables ManagedServices to support Concurrent multi user environment"
+  bug: "380297485"
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index d538bb8..c3af578 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -176,16 +176,13 @@
             if (Flags.bicClient()) {
                 mService.enforceCallerPermissions();
             }
-            if (!Build.IS_DEBUGGABLE) {
-                return mService.getBackgroundInstalledPackages(flags, userId);
-            }
             // The debug.transparency.bg-install-apps (only works for debuggable builds)
             // is used to set mock list of background installed apps for testing.
             // The list of apps' names is delimited by ",".
             // TODO: Remove after migrating test to new background install method using
             // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905
             String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
-            if (TextUtils.isEmpty(propertyString)) {
+            if (TextUtils.isEmpty(propertyString) || !Build.IS_DEBUGGABLE) {
                 return mService.getBackgroundInstalledPackages(flags, userId);
             } else {
                 return mService.getMockBackgroundInstalledPackages(propertyString);
@@ -219,10 +216,27 @@
                     PackageManager.PackageInfoFlags.of(flags), userId);
 
             initBackgroundInstalledPackages();
+            if(Build.IS_DEBUGGABLE) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("Tracked background installed package size: ")
+                    .append(mBackgroundInstalledPackages.size())
+                    .append("\n");
+                for (int i = 0; i < mBackgroundInstalledPackages.size(); ++i) {
+                    int installingUserId = mBackgroundInstalledPackages.keyAt(i);
+                    mBackgroundInstalledPackages.get(installingUserId).forEach(pkgName ->
+                        sb.append("userId: ").append(installingUserId)
+                            .append(", name: ").append(pkgName).append("\n"));
+                }
+                Slog.d(TAG, "Tracked background installed package: " + sb.toString());
+            }
+
             ListIterator<PackageInfo> iter = packages.listIterator();
             while (iter.hasNext()) {
                 String packageName = iter.next().packageName;
                 if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
+                    if(Build.IS_DEBUGGABLE) {
+                        Slog.d(TAG,  packageName + " is not tracked, removing");
+                    }
                     iter.remove();
                 }
             }
@@ -284,6 +298,9 @@
     }
 
     void handlePackageAdd(String packageName, int userId) {
+        if(Build.IS_DEBUGGABLE) {
+            Slog.d(TAG, "handlePackageAdd: checking " + packageName);
+        }
         ApplicationInfo appInfo = null;
         try {
             appInfo =
@@ -302,7 +319,7 @@
             installerPackageName = installInfo.getInstallingPackageName();
             initiatingPackageName = installInfo.getInitiatingPackageName();
         } catch (PackageManager.NameNotFoundException e) {
-            Slog.w(TAG, "Package's installer not found " + packageName);
+            Slog.w(TAG, "Package's installer not found: " + packageName);
             return;
         }
 
@@ -314,6 +331,10 @@
                 VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT,
                 userId)
                 != PERMISSION_GRANTED) {
+            if(Build.IS_DEBUGGABLE) {
+                Slog.d(TAG, "handlePackageAdd " + packageName + ": installer doesn't "
+                    + "have INSTALL_PACKAGES permission, skipping");
+            }
             return;
         }
 
@@ -324,6 +345,10 @@
 
         if (installedByAdb(initiatingPackageName)
                 || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
+            if(Build.IS_DEBUGGABLE) {
+                Slog.d(TAG, "handlePackageAdd " + packageName + ": is installed by ADB or was "
+                    + "foreground installation, skipping");
+            }
             return;
         }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4153cd1..76c5240 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1163,15 +1163,6 @@
         }
     }
 
-    private boolean shouldShowHub() {
-        final boolean hubEnabled = Settings.Secure.getIntForUser(
-                mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED,
-                1, mCurrentUserId) == 1;
-
-        return mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled
-                && mDreamManagerInternal.dreamConditionActive();
-    }
-
     @VisibleForTesting
     void powerPress(long eventTime, int count, int displayId) {
         // SideFPS still needs to know about suppressed power buttons, in case it needs to block
@@ -1270,10 +1261,9 @@
                     // show hub.
                     boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled(
                             mCurrentUserId);
-                    if (shouldShowHub() && keyguardAvailable) {
-                        // If the hub can be launched, send a message to keyguard. We do not know if
-                        // the hub is already running or not, keyguard handles turning screen off if
-                        // it is.
+                    if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled
+                            && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) {
+                        // If the hub can be launched, send a message to keyguard.
                         Bundle options = new Bundle();
                         options.putBoolean(EXTRA_TRIGGER_HUB, true);
                         lockNow(options);
@@ -1334,14 +1324,14 @@
      * @param isScreenOn Whether the screen is currently on.
      * @param noDreamAction The action to perform if dreaming is not possible.
      */
-    private boolean attemptToDreamFromShortPowerButtonPress(
+    private void attemptToDreamFromShortPowerButtonPress(
             boolean isScreenOn, Runnable noDreamAction) {
         if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
                 && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
             // If the power button behavior isn't one that should be able to trigger the dream, give
             // up.
             noDreamAction.run();
-            return false;
+            return;
         }
 
         final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
@@ -1349,7 +1339,7 @@
             Slog.d(TAG, "Can't start dreaming when attempting to dream from short power"
                     + " press (isScreenOn=" + isScreenOn + ")");
             noDreamAction.run();
-            return false;
+            return;
         }
 
         synchronized (mLock) {
@@ -1360,8 +1350,6 @@
         }
 
         dreamManagerInternal.requestDream();
-
-        return true;
     }
 
     /**
@@ -6410,17 +6398,6 @@
                 event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) {
             return;
         }
-
-        if (!shouldShowHub()
-                && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP
-                && event.getKeyCode() == KEYCODE_POWER
-                && attemptToDreamFromShortPowerButtonPress(false, () -> {})) {
-            // In the case that we should wake to dream and successfully initiate dreaming, do not
-            // continue waking up. Doing so will exit the dream state and cause UI to react
-            // accordingly.
-            return;
-        }
-
         wakeUpFromWakeKey(
                 event.getEventTime(),
                 event.getKeyCode(),
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 8fae875..e3eced2 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -3379,7 +3379,7 @@
                 }
                 changed = sleepPowerGroupLocked(powerGroup, time,
                         PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID);
-            } else if (shouldNapAtBedTimeLocked()) {
+            } else if (shouldNapAtBedTimeLocked(powerGroup)) {
                 changed = dreamPowerGroupLocked(powerGroup, time,
                         Process.SYSTEM_UID, /* allowWake= */ false);
             } else {
@@ -3395,7 +3395,10 @@
      * activity timeout has expired and it's bedtime.
      */
     @GuardedBy("mLock")
-    private boolean shouldNapAtBedTimeLocked() {
+    private boolean shouldNapAtBedTimeLocked(PowerGroup powerGroup) {
+        if (!powerGroup.supportsSandmanLocked()) {
+            return false;
+        }
         return mDreamsActivateOnSleepSetting
                 || (mDreamsActivateOnDockSetting
                         && mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED)
@@ -3617,9 +3620,10 @@
         if (!mDreamsDisabledByAmbientModeSuppressionConfig) {
             return;
         }
+        final PowerGroup defaultPowerGroup = mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP);
         if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting
-                && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked(
-                mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) {
+                && shouldNapAtBedTimeLocked(defaultPowerGroup)
+                && isItBedTimeYetLocked(defaultPowerGroup)) {
             napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true);
         } else if (isSuppressed) {
             mDirty |= DIRTY_SETTINGS;
diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
index f060e4d..82df310 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
@@ -303,7 +303,11 @@
         if (mRevocationEnabled) {
             // Checks Revocation Status List based on
             // https://developer.android.com/training/articles/security-key-attestation#certificate_status
-            mCertificateRevocationStatusManager.checkRevocationStatus(certificates);
+            // The first certificate is the leaf, which is generated at runtime with the attestation
+            // attributes such as the challenge. It is specific to this attestation instance and
+            // does not need to be checked for revocation.
+            mCertificateRevocationStatusManager.checkRevocationStatus(
+                    new ArrayList<>(certificates.subList(1, certificates.size())));
         }
     }
 
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
index d36d9f5..4cd4b3b 100644
--- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -42,6 +42,7 @@
 import java.net.URL;
 import java.security.cert.CertPathValidatorException;
 import java.security.cert.X509Certificate;
+import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -67,6 +68,8 @@
      */
     @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30;
 
+    @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_CHECK = 24;
+
     /**
      * The number of days since issue date for an intermediary certificate to be considered fresh
      * and not require a revocation list check.
@@ -127,6 +130,17 @@
             serialNumbers.add(serialNumber);
         }
         try {
+            if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) {
+                Slog.d(
+                        TAG,
+                        "All certificates have been checked for revocation recently. No need to"
+                                + " check this time.");
+                return;
+            }
+        } catch (IOException ignored) {
+            // Proceed to check the revocation status
+        }
+        try {
             JSONObject revocationList = fetchRemoteRevocationList();
             Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
             for (String serialNumber : serialNumbers) {
@@ -151,25 +165,32 @@
                     serialNumbers.remove(serialNumber);
                 }
             }
-            Map<String, LocalDateTime> lastRevocationCheckData;
             try {
-                lastRevocationCheckData = getLastRevocationCheckData();
+                if (!isLastCheckedWithin(
+                        Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) {
+                    throw new CertPathValidatorException(
+                            "Unable to verify the revocation status of one of the certificates "
+                                    + serialNumbers);
+                }
             } catch (IOException ex2) {
                 throw new CertPathValidatorException(
                         "Unable to load stored revocation status", ex2);
             }
-            for (String serialNumber : serialNumbers) {
-                if (!lastRevocationCheckData.containsKey(serialNumber)
-                        || lastRevocationCheckData
-                                .get(serialNumber)
-                                .isBefore(
-                                        LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) {
-                    throw new CertPathValidatorException(
-                            "Unable to verify the revocation status of certificate "
-                                    + serialNumber);
-                }
+        }
+    }
+
+    private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers)
+            throws IOException {
+        Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData();
+        for (String serialNumber : serialNumbers) {
+            if (!lastRevocationCheckData.containsKey(serialNumber)
+                    || lastRevocationCheckData
+                            .get(serialNumber)
+                            .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) {
+                return false;
             }
         }
+        return true;
     }
 
     private static boolean needToCheckRevocationStatus(
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index bfd86d7..9f9a980 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -54,11 +54,6 @@
             super(PermissionEnforcer.fromContext(context));
         }
 
-        @Override
-        public boolean isApkVeritySupported() {
-            return VerityUtils.isFsVeritySupported();
-        }
-
         private void checkCallerPackageName(String packageName) {
             final int callingUid = Binder.getCallingUid();
             final int callingUserId = UserHandle.getUserId(callingUid);
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 6cc17d4..a941838 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -857,8 +857,7 @@
         info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
         info.mIsDrawn = true;
         final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
-        if (info.mLoggedTransitionStarting || (!r.mDisplayContent.mOpeningApps.contains(r)
-                && !r.mTransitionController.isCollecting(r))) {
+        if (info.mLoggedTransitionStarting || !r.mTransitionController.isCollecting(r)) {
             done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index c37b5a0..333d91a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -159,7 +159,6 @@
 import static com.android.server.wm.ActivityRecordProto.IN_SIZE_COMPAT_MODE;
 import static com.android.server.wm.ActivityRecordProto.IS_ANIMATING;
 import static com.android.server.wm.ActivityRecordProto.IS_USER_FULLSCREEN_OVERRIDE_ENABLED;
-import static com.android.server.wm.ActivityRecordProto.IS_WAITING_FOR_TRANSITION_START;
 import static com.android.server.wm.ActivityRecordProto.LAST_ALL_DRAWN;
 import static com.android.server.wm.ActivityRecordProto.LAST_DROP_INPUT_MODE;
 import static com.android.server.wm.ActivityRecordProto.LAST_SURFACE_SHOWING;
@@ -330,7 +329,6 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.WindowInsets;
-import android.view.WindowInsets.Type;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
@@ -1519,17 +1517,7 @@
         this.task = newTask;
 
         if (shouldStartChangeTransition(newParent, oldParent)) {
-            if (mTransitionController.isShellTransitionsEnabled()) {
-                // For Shell transition, call #initializeChangeTransition directly to take the
-                // screenshot at the Activity level. And Shell will be in charge of handling the
-                // surface reparent and crop.
-                initializeChangeTransition(getBounds());
-            } else {
-                // For legacy app transition, we want to take a screenshot of the Activity surface,
-                // but animate the change transition on TaskFragment level to get the correct window
-                // crop.
-                newParent.initializeChangeTransition(getBounds(), getSurfaceControl());
-            }
+            mTransitionController.collectVisibleChange(this);
         }
 
         super.onParentChanged(newParent, oldParent);
@@ -1557,16 +1545,6 @@
             mLastReportedPictureInPictureMode = inPinnedWindowingMode();
         }
 
-        // When the associated task is {@code null}, the {@link ActivityRecord} can no longer
-        // access visual elements like the {@link DisplayContent}. We must remove any associations
-        // such as animations.
-        if (task == null) {
-            // It is possible we have been marked as a closing app earlier. We must remove ourselves
-            // from this list so we do not participate in any future animations.
-            if (getDisplayContent() != null) {
-                getDisplayContent().mClosingApps.remove(this);
-            }
-        }
         final Task rootTask = getRootTask();
         if (task == mLastParentBeforePip && task != null) {
             // Notify the TaskFragmentOrganizer that the activity is reparented back from pip.
@@ -1749,14 +1727,6 @@
             return;
         }
         prevDc.onRunningActivityChanged();
-
-        if (prevDc.mOpeningApps.remove(this)) {
-            // Transfer opening transition to new display.
-            mDisplayContent.mOpeningApps.add(this);
-            mDisplayContent.executeAppTransition();
-        }
-
-        prevDc.mClosingApps.remove(this);
         prevDc.getDisplayPolicy().removeRelaunchingApp(this);
 
         if (prevDc.mFocusedApp == this) {
@@ -4392,7 +4362,6 @@
 
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Removing app token: %s", this);
 
-        getDisplayContent().mOpeningApps.remove(this);
         getDisplayContent().mUnknownAppVisibilityController.appRemovedOrHidden(this);
         mWmService.mSnapshotController.onAppRemoved(this);
         mAtmService.mStartingProcessActivities.remove(this);
@@ -4404,20 +4373,9 @@
         mAppCompatController.getTransparentPolicy().stop();
 
         // Defer removal of this activity when either a child is animating, or app transition is on
-        // going. App transition animation might be applied on the parent task not on the activity,
-        // but the actual frame buffer is associated with the activity, so we have to keep the
-        // activity while a parent is animating.
-        boolean delayed = isAnimating(TRANSITION | PARENTS | CHILDREN,
-                ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION);
-        if (getDisplayContent().mClosingApps.contains(this)) {
-            delayed = true;
-        } else if (getDisplayContent().mAppTransition.isTransitionSet()) {
-            getDisplayContent().mClosingApps.add(this);
-            delayed = true;
-        } else if (mTransitionController.inTransition()) {
-            delayed = true;
-        }
-
+        // going. The handleCompleteDeferredRemoval will continue the removal.
+        final boolean delayed = isAnimating(CHILDREN, ANIMATION_TYPE_WINDOW_ANIMATION)
+                || mTransitionController.inTransition();
         // Don't commit visibility if it is waiting to animate. It will be set post animation.
         if (!delayed) {
             commitVisibility(false /* visible */, true /* performLayout */);
@@ -5552,9 +5510,6 @@
 
         mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible);
 
-        final DisplayContent displayContent = getDisplayContent();
-        displayContent.mOpeningApps.remove(this);
-        displayContent.mClosingApps.remove(this);
         setVisibleRequested(visible);
         mLastDeferHidingClient = deferHidingClient;
 
@@ -5567,13 +5522,6 @@
                 setClientVisible(false);
             }
         } else {
-            if (!appTransition.isTransitionSet()
-                    && appTransition.isReady()) {
-                // Add the app mOpeningApps if transition is unset but ready. This means
-                // we're doing a screen freeze, and the unfreeze will wait for all opening
-                // apps to be ready.
-                displayContent.mOpeningApps.add(this);
-            }
             startingMoved = false;
             // If the token is currently hidden (should be the common case), or has been
             // stopped, then we need to set up to wait for its windows to be ready.
@@ -6775,7 +6723,7 @@
         setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM, "checkAppWindowsReadyToShow");
 
         // We can now show all of the drawn windows!
-        if (!getDisplayContent().mOpeningApps.contains(this) && canShowWindows()) {
+        if (canShowWindows()) {
             showAllWindowsLocked();
         }
     }
@@ -7449,15 +7397,6 @@
         return boundsLayer;
     }
 
-    @Override
-    boolean isWaitingForTransitionStart() {
-        final DisplayContent dc = getDisplayContent();
-        return dc != null && dc.mAppTransition.isTransitionSet()
-                && (dc.mOpeningApps.contains(this)
-                || dc.mClosingApps.contains(this)
-                || dc.mChangingContainers.contains(this));
-    }
-
     boolean isTransitionForward() {
         return (mStartingData != null && mStartingData.mIsTransitionForward)
                 || mDisplayContent.isNextTransitionForward();
@@ -9561,7 +9500,6 @@
         writeNameToProto(proto, NAME);
         super.dumpDebug(proto, WINDOW_TOKEN, logLevel);
         proto.write(LAST_SURFACE_SHOWING, mLastSurfaceShowing);
-        proto.write(IS_WAITING_FOR_TRANSITION_START, isWaitingForTransitionStart());
         proto.write(IS_ANIMATING, isAnimating(TRANSITION | PARENTS | CHILDREN,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION));
         proto.write(FILLS_PARENT, fillsParent());
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 6a5adca..b607b0f 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -145,6 +145,7 @@
 import android.view.Display;
 import android.webkit.URLUtil;
 import android.window.ActivityWindowInfo;
+import android.window.DesktopExperienceFlags;
 import android.window.DesktopModeFlags;
 
 import com.android.internal.R;
@@ -2916,6 +2917,8 @@
 
     /** The helper to calculate whether a container is opaque. */
     static class OpaqueContainerHelper implements Predicate<ActivityRecord> {
+        private final boolean mEnableMultipleDesktopsBackend =
+                DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue();
         private ActivityRecord mStarting;
         private boolean mIgnoringInvisibleActivity;
         private boolean mIgnoringKeyguard;
@@ -2938,7 +2941,7 @@
             mIgnoringKeyguard = ignoringKeyguard;
 
             final boolean isOpaque;
-            if (!Flags.enableMultipleDesktopsBackend()) {
+            if (!mEnableMultipleDesktopsBackend) {
                 isOpaque = container.getActivity(this,
                         true /* traverseTopToBottom */, null /* boundary */) != null;
             } else {
@@ -2949,13 +2952,16 @@
         }
 
         private boolean isOpaqueInner(@NonNull WindowContainer<?> container) {
-            // If it's a leaf task fragment, then opacity is calculated based on its activities.
-            if (container.asTaskFragment() != null
-                    && ((TaskFragment) container).isLeafTaskFragment()) {
+            final boolean isActivity = container.asActivityRecord() != null;
+            final boolean isLeafTaskFragment = container.asTaskFragment() != null
+                    && ((TaskFragment) container).isLeafTaskFragment();
+            if (isActivity || isLeafTaskFragment) {
+                // When it is an activity or leaf task fragment, then opacity is calculated based
+                // on itself or its activities.
                 return container.getActivity(this,
                         true /* traverseTopToBottom */, null /* boundary */) != null;
             }
-            // When not a leaf, it's considered opaque if any of its opaque children fill this
+            // Otherwise, it's considered opaque if any of its opaque children fill this
             // container, unless the children are adjacent fragments, in which case as long as they
             // are all opaque then |container| is also considered opaque, even if the adjacent
             // task fragment aren't filling.
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
index dff072e..57811e2 100644
--- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
@@ -16,12 +16,14 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.window.DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS;
 
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
 import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString;
@@ -48,6 +50,8 @@
  */
 class AppCompatLetterboxPolicy {
 
+    private static final int DIFF_TOLERANCE_PX = 1;
+
     @NonNull
     private final ActivityRecord mActivityRecord;
     @NonNull
@@ -56,6 +60,9 @@
     private final AppCompatRoundedCorners mAppCompatRoundedCorners;
     @NonNull
     private final AppCompatConfiguration mAppCompatConfiguration;
+    // Convenience temporary object to save allocation when calculating Rect.
+    @NonNull
+    private final Rect mTmpRect = new Rect();
 
     private boolean mLastShouldShowLetterboxUi;
 
@@ -71,7 +78,7 @@
                 : new LegacyLetterboxPolicyState();
         // TODO (b/358334569) Improve cutout logic dependency on app compat.
         mAppCompatRoundedCorners = new AppCompatRoundedCorners(mActivityRecord,
-                this::isLetterboxedNotForDisplayCutout);
+                this::ieEligibleForRoundedCorners);
         mAppCompatConfiguration = appCompatConfiguration;
     }
 
@@ -84,7 +91,7 @@
         mLetterboxPolicyState.stop();
     }
 
-    /** @return {@value true} if the letterbox policy is running and the activity letterboxed. */
+    /** @return {@code true} if the letterbox policy is running and the activity letterboxed. */
     boolean isRunning() {
         return mLetterboxPolicyState.isRunning();
     }
@@ -130,7 +137,7 @@
      *     <li>The activity is in fullscreen.
      *     <li>The activity is portrait-only.
      *     <li>The activity doesn't have a starting window (education should only be displayed
-     *     once the starting window is removed in {@link #removeStartingWindow}).
+     *     once the starting window is removed in {@link ActivityRecord#removeStartingWindow}).
      * </ul>
      */
     boolean isEligibleForLetterboxEducation() {
@@ -294,16 +301,40 @@
         }
     }
 
+    private boolean ieEligibleForRoundedCorners(@NonNull WindowState mainWindow) {
+        return isLetterboxedNotForDisplayCutout(mainWindow)
+                && !isFreeformActivityMatchParentAppBoundsHeight();
+    }
+
     private boolean isLetterboxedNotForDisplayCutout(@NonNull WindowState mainWindow) {
         return shouldShowLetterboxUi(mainWindow)
                 && !mainWindow.isLetterboxedForDisplayCutout();
     }
 
+    private boolean isFreeformActivityMatchParentAppBoundsHeight() {
+        if (!EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue()) {
+            return false;
+        }
+        final Task task = mActivityRecord.getTask();
+        if (task == null) {
+            return false;
+        }
+        final Rect parentAppBounds = task.getWindowConfiguration().getAppBounds();
+        if (parentAppBounds == null) {
+            return false;
+        }
+
+        mLetterboxPolicyState.getLetterboxInnerBounds(mTmpRect);
+        final int diff = parentAppBounds.height() - mTmpRect.height();
+        // Compare bounds with tolerance of 1 px to account for rounding error calculations.
+        return task.getWindowingMode() == WINDOWING_MODE_FREEFORM && diff <= DIFF_TOLERANCE_PX;
+    }
+
     private static boolean shouldNotLayoutLetterbox(@Nullable WindowState w) {
         if (w == null) {
             return true;
         }
-        final int type = w.mAttrs.type;
+        final int type = w.getAttrs().type;
         // Allow letterbox to be displayed early for base application or application starting
         // windows even if it is not on the top z order to prevent flickering when the
         // letterboxed window is brought to the top
diff --git a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java
index 92d76e5..8165638 100644
--- a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java
+++ b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java
@@ -35,12 +35,12 @@
     @NonNull
     private final ActivityRecord mActivityRecord;
     @NonNull
-    private final Predicate<WindowState> mIsLetterboxedNotForDisplayCutout;
+    private final Predicate<WindowState> mRoundedCornersWindowCondition;
 
     AppCompatRoundedCorners(@NonNull ActivityRecord  activityRecord,
-            @NonNull Predicate<WindowState> isLetterboxedNotForDisplayCutout) {
+            @NonNull Predicate<WindowState> roundedCornersWindowCondition) {
         mActivityRecord = activityRecord;
-        mIsLetterboxedNotForDisplayCutout = isLetterboxedNotForDisplayCutout;
+        mRoundedCornersWindowCondition = roundedCornersWindowCondition;
     }
 
     void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow) {
@@ -136,7 +136,7 @@
     private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) {
         final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
                 .mAppCompatController.getLetterboxOverrides();
-        return mIsLetterboxedNotForDisplayCutout.test(mainWindow)
+        return mRoundedCornersWindowCondition.test(mainWindow)
                 && letterboxOverrides.isLetterboxActivityCornersRounded();
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java
index 26cf32b..6a46f57 100644
--- a/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.wm;
 
+import static android.window.DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS;
+
 import static com.android.server.wm.AppCompatUtils.isInDesktopMode;
 
 import android.annotation.NonNull;
@@ -22,8 +24,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 
-import com.android.window.flags.Flags;
-
 /**
  * Encapsulate logic related to sandboxing for app compatibility.
  */
@@ -48,7 +48,7 @@
      */
     void sandboxBoundsIfNeeded(@NonNull Configuration resolvedConfig,
             @WindowingMode int windowingMode) {
-        if (!Flags.excludeCaptionFromAppBounds()) {
+        if (!EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue()) {
             return;
         }
 
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index d98ad8b..12d4a21 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -89,7 +89,6 @@
 import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation;
 import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM;
 import static com.android.server.wm.AppTransitionProto.APP_TRANSITION_STATE;
 import static com.android.server.wm.AppTransitionProto.LAST_USED_APP_TRANSITION;
@@ -1554,26 +1553,6 @@
     }
 
     private void handleAppTransitionTimeout() {
-        synchronized (mService.mGlobalLock) {
-            final DisplayContent dc = mDisplayContent;
-            if (dc == null) {
-                return;
-            }
-            notifyAppTransitionTimeoutLocked();
-            if (isTransitionSet() || !dc.mOpeningApps.isEmpty() || !dc.mClosingApps.isEmpty()
-                    || !dc.mChangingContainers.isEmpty()) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                            "*** APP TRANSITION TIMEOUT. displayId=%d isTransitionSet()=%b "
-                                    + "mOpeningApps.size()=%d mClosingApps.size()=%d "
-                                    + "mChangingApps.size()=%d",
-                            dc.getDisplayId(), dc.mAppTransition.isTransitionSet(),
-                            dc.mOpeningApps.size(), dc.mClosingApps.size(),
-                            dc.mChangingContainers.size());
-
-                setTimeout();
-                mService.mWindowPlacerLocked.performSurfacePlacement();
-            }
-        }
     }
 
     private static void doAnimationCallback(@NonNull IRemoteCallback callback) {
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 05dcbb7..aaa18ad 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -257,10 +257,12 @@
         // This should be the only place override the configuration for ActivityRecord. Override
         // the value if not calculated yet.
         Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+        Rect outConfigBounds = new Rect(outAppBounds);
         if (outAppBounds == null || outAppBounds.isEmpty()) {
             inOutConfig.windowConfiguration.setAppBounds(
                     newParentConfiguration.windowConfiguration.getBounds());
             outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+            outConfigBounds.set(outAppBounds);
             if (task != null) {
                 task = task.getCreatedByOrganizerTask();
                 if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
@@ -279,6 +281,12 @@
                     outAppBounds.inset(decor.mOverrideNonDecorInsets);
                 }
             }
+            if (!outConfigBounds.intersect(decor.mOverrideConfigFrame)) {
+                if (inOutConfig.windowConfiguration.getWindowingMode()
+                        == WINDOWING_MODE_MULTI_WINDOW) {
+                    outAppBounds.inset(decor.mOverrideConfigInsets);
+                }
+            }
             if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
                 outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets);
             }
@@ -289,10 +297,10 @@
         }
         density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
         if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
-            inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
+            inOutConfig.screenWidthDp = (int) (outConfigBounds.width() / density + 0.5f);
         }
         if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-            inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
+            inOutConfig.screenHeightDp = (int) (outConfigBounds.height() / density + 0.5f);
         }
         if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
                 && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 4eaa11b..f473b7b 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -60,10 +60,11 @@
      */
     @VisibleForTesting
     static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> {
-        // Treat unique id and address change as WM-specific display change as we re-query display
-        // settings and parameters based on it which could cause window changes
+        // Treat unique id, address, and canHostTasks change as WM-specific display change as we
+        // re-query display settings and parameters based on it which could cause window changes.
         out.uniqueId = override.uniqueId;
         out.address = override.address;
+        out.canHostTasks = override.canHostTasks;
 
         // Also apply WM-override fields, since they might produce differences in window hierarchy
         WM_OVERRIDE_FIELDS.setFields(out, override);
@@ -433,7 +434,7 @@
                 second.thermalRefreshRateThrottling)
                 || !Objects.equals(first.thermalBrightnessThrottlingDataId,
                 second.thermalBrightnessThrottlingDataId)
-                || first.canHostTasks != second.canHostTasks) {
+        ) {
             diff |= DIFF_NOT_WM_DEFERRABLE;
         }
 
@@ -454,6 +455,7 @@
                 || !Objects.equals(first.displayShape, second.displayShape)
                 || !Objects.equals(first.uniqueId, second.uniqueId)
                 || !Objects.equals(first.address, second.address)
+                || first.canHostTasks != second.canHostTasks
         ) {
             diff |= DIFF_WM_DEFERRABLE;
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 7ab3fde..7eebbba 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -108,7 +108,6 @@
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
 import static com.android.server.wm.DisplayContentProto.APP_TRANSITION;
-import static com.android.server.wm.DisplayContentProto.CLOSING_APPS;
 import static com.android.server.wm.DisplayContentProto.CURRENT_FOCUS;
 import static com.android.server.wm.DisplayContentProto.DISPLAY_FRAMES;
 import static com.android.server.wm.DisplayContentProto.DISPLAY_INFO;
@@ -125,7 +124,6 @@
 import static com.android.server.wm.DisplayContentProto.IS_SLEEPING;
 import static com.android.server.wm.DisplayContentProto.KEEP_CLEAR_AREAS;
 import static com.android.server.wm.DisplayContentProto.MIN_SIZE_OF_RESIZEABLE_TASK_DP;
-import static com.android.server.wm.DisplayContentProto.OPENING_APPS;
 import static com.android.server.wm.DisplayContentProto.RESUMED_ACTIVITY;
 import static com.android.server.wm.DisplayContentProto.ROOT_DISPLAY_AREA;
 import static com.android.server.wm.DisplayContentProto.SLEEP_TOKENS;
@@ -164,6 +162,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.content.ComponentCallbacks;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -196,7 +195,6 @@
 import android.os.UserManager;
 import android.os.WorkSource;
 import android.provider.Settings;
-import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.DisplayUtils;
@@ -367,15 +365,7 @@
 
     final AppTransition mAppTransition;
 
-    final ArraySet<ActivityRecord> mOpeningApps = new ArraySet<>();
-    final ArraySet<ActivityRecord> mClosingApps = new ArraySet<>();
-    final ArraySet<WindowContainer> mChangingContainers = new ArraySet<>();
     final UnknownAppVisibilityController mUnknownAppVisibilityController;
-    /**
-     * If a container is closing when resizing, keeps track of its starting bounds when it is
-     * removed from {@link #mChangingContainers}.
-     */
-    final ArrayMap<WindowContainer, Rect> mClosingChangingContainers = new ArrayMap<>();
 
     private MetricsLogger mMetricsLogger;
 
@@ -467,6 +457,7 @@
     private DisplayInfo mLastDisplayInfoOverride;
 
     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+    @NonNull
     private final DisplayPolicy mDisplayPolicy;
     private final DisplayRotation mDisplayRotation;
 
@@ -553,6 +544,7 @@
     /** Remove this display when animation on it has completed. */
     private boolean mDeferredRemoval;
 
+    @NonNull
     final PinnedTaskController mPinnedTaskController;
 
     private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();
@@ -1113,6 +1105,29 @@
     };
 
     /**
+     * Called to update fields retrieve from {@link #getDisplayUiContext()} resources when
+     * there's a configuration update on {@link #getDisplayUiContext()}.
+     */
+    @NonNull
+    private final ComponentCallbacks mSysUiContextConfigCallback = new ComponentCallbacks() {
+
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            synchronized (mWmService.mGlobalLock) {
+                if (mDisplayReady) {
+                    mDisplayPolicy.onConfigurationChanged();
+                    mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
+                }
+            }
+        }
+
+        @Override
+        public void onLowMemory() {
+            // Do nothing.
+        }
+    };
+
+    /**
      * Create new {@link DisplayContent} instance, add itself to the root window container and
      * initialize direct children.
      * @param display May not be null.
@@ -1910,17 +1925,10 @@
             return false;
         }
         if (checkOpening) {
-            if (mTransitionController.isShellTransitionsEnabled()) {
-                if (!mTransitionController.isCollecting(r)) {
-                    return false;
-                }
-            } else {
-                if (!mAppTransition.isTransitionSet() || !mOpeningApps.contains(r)) {
-                    // Apply normal rotation animation in case of the activity set different
-                    // requested orientation without activity switch, or the transition is unset due
-                    // to starting window was transferred ({@link #mSkipAppTransitionAnimation}).
-                    return false;
-                }
+            if (!mTransitionController.isCollecting(r)) {
+                // Apply normal rotation animation in case the activity changes requested
+                // orientation without activity switch.
+                return false;
             }
             if (r.isState(RESUMED) && !r.getTask().mInResumeTopActivity) {
                 // If the activity is executing or has done the lifecycle callback, use normal
@@ -2815,11 +2823,10 @@
         final int lastOrientation = getConfiguration().orientation;
         final int lastWindowingMode = getWindowingMode();
         super.onConfigurationChanged(newParentConfig);
-        if (mDisplayPolicy != null) {
-            mDisplayPolicy.onConfigurationChanged();
-            mPinnedTaskController.onPostDisplayConfigurationChanged();
-            mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
+        if (!Flags.trackSystemUiContextBeforeWms()) {
+            mSysUiContextConfigCallback.onConfigurationChanged(newParentConfig);
         }
+        mPinnedTaskController.onPostDisplayConfigurationChanged();
         // Update IME parent if needed.
         updateImeParent();
 
@@ -3239,25 +3246,48 @@
             Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off.");
         }
 
-        final boolean shouldShow;
-        if (isDefaultDisplay) {
-            shouldShow = true;
-        } else if (isPrivate()) {
-            shouldShow = false;
-        } else {
-            shouldShow = mDisplay.canHostTasks();
-        }
-
-        if (shouldShow == mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) {
+        final boolean shouldShowContent;
+        if (!allowContentModeSwitch()) {
             return;
         }
-        mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShow);
+        shouldShowContent = mDisplay.canHostTasks();
 
-        if (!shouldShow) {
+        if (shouldShowContent == mWmService.mDisplayWindowSettings
+                .shouldShowSystemDecorsLocked(this)) {
+            return;
+        }
+        mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShowContent);
+
+        if (!shouldShowContent) {
             clearAllTasksOnDisplay(null /* clearTasksCallback */, false /* isRemovingDisplay */);
         }
     }
 
+     /**
+      * Note that we only allow displays that are able to show system decorations to use the content
+      * mode switch; however, not all displays that are able to show system decorations are allowed
+      * to use the content mode switch.
+      */
+     boolean allowContentModeSwitch() {
+        // The default display should always show system decorations.
+        if (isDefaultDisplay) {
+            return false;
+        }
+
+        // Private display should never show system decorations.
+        if (isPrivate()) {
+            return false;
+        }
+
+        // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_SYSTEM_DECORATIONS_CHANGE.
+        // Virtual displays cannot add or remove system decorations during their lifecycle.
+        if (mDisplay.getType() == Display.TYPE_VIRTUAL) {
+            return false;
+        }
+
+        return true;
+    }
+
     DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) {
         if (mDisplayPolicy == null || mInitialDisplayCutout == null) {
             return null;
@@ -3354,10 +3384,6 @@
     void removeImmediately() {
         mDeferredRemoval = false;
         try {
-            // Clear all transitions & screen frozen states when removing display.
-            mOpeningApps.clear();
-            mClosingApps.clear();
-            mChangingContainers.clear();
             mUnknownAppVisibilityController.clear();
             mAppTransition.removeAppTransitionTimeoutCallbacks();
             mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
@@ -3380,6 +3406,9 @@
                     .getKeyguardController().onDisplayRemoved(mDisplayId);
             mWallpaperController.resetLargestDisplay(mDisplay);
             mWmService.mDisplayWindowSettings.onDisplayRemoved(this);
+            if (Flags.trackSystemUiContextBeforeWms()) {
+                getDisplayUiContext().unregisterComponentCallbacks(mSysUiContextConfigCallback);
+            }
         } finally {
             mDisplayReady = false;
         }
@@ -3549,12 +3578,6 @@
         if (mFocusedApp != null) {
             mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
         }
-        for (int i = mOpeningApps.size() - 1; i >= 0; i--) {
-            mOpeningApps.valueAt(i).writeIdentifierToProto(proto, OPENING_APPS);
-        }
-        for (int i = mClosingApps.size() - 1; i >= 0; i--) {
-            mClosingApps.valueAt(i).writeIdentifierToProto(proto, CLOSING_APPS);
-        }
 
         final Task focusedRootTask = getFocusedRootTask();
         if (focusedRootTask != null) {
@@ -4813,19 +4836,6 @@
             }
         }
 
-        if (!mOpeningApps.isEmpty() || !mClosingApps.isEmpty() || !mChangingContainers.isEmpty()) {
-            pw.println();
-            if (mOpeningApps.size() > 0) {
-                pw.print("  mOpeningApps="); pw.println(mOpeningApps);
-            }
-            if (mClosingApps.size() > 0) {
-                pw.print("  mClosingApps="); pw.println(mClosingApps);
-            }
-            if (mChangingContainers.size() > 0) {
-                pw.print("  mChangingApps="); pw.println(mChangingContainers);
-            }
-        }
-
         mUnknownAppVisibilityController.dump(pw, "  ");
     }
 
@@ -5447,7 +5457,7 @@
             reconfigureDisplayLocked();
             onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration());
             mWmService.mDisplayNotificationController.dispatchDisplayAdded(this);
-            // Attach the SystemUiContext to this DisplayContent the get latest configuration.
+            // Attach the SystemUiContext to this DisplayContent to get latest configuration.
             // Note that the SystemUiContext will be removed automatically if this DisplayContent
             // is detached.
             registerSystemUiContext();
@@ -5455,11 +5465,15 @@
     }
 
     private void registerSystemUiContext() {
+        final Context systemUiContext = getDisplayUiContext();
         final WindowProcessController wpc = mAtmService.getProcessController(
-                getDisplayUiContext().getIApplicationThread());
+                systemUiContext.getIApplicationThread());
         mWmService.mWindowContextListenerController.registerWindowContainerListener(
-                wpc, getDisplayUiContext().getWindowContextToken(), this,
+                wpc, systemUiContext.getWindowContextToken(), this,
                 INVALID_WINDOW_TYPE, null /* options */);
+        if (Flags.trackSystemUiContextBeforeWms()) {
+            systemUiContext.registerComponentCallbacks(mSysUiContextConfigCallback);
+        }
     }
 
     @Override
@@ -6638,6 +6652,7 @@
         forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); });
     }
 
+    @NonNull
     Context getDisplayUiContext() {
         return mDisplayPolicy.getSystemUiContext();
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 10f591c..fbe8501 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1865,6 +1865,7 @@
         return mContext;
     }
 
+    @NonNull
     Context getSystemUiContext() {
         return mUiContext;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 539fc12..1173875 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -247,12 +247,7 @@
 
     void setShouldShowSystemDecorsLocked(@NonNull DisplayContent dc, boolean shouldShow) {
         final boolean changed = (shouldShow != shouldShowSystemDecorsLocked(dc));
-
-        final DisplayInfo displayInfo = dc.getDisplayInfo();
-        final SettingsProvider.SettingsEntry overrideSettings =
-                mSettingsProvider.getOverrideSettings(displayInfo);
-        overrideSettings.mShouldShowSystemDecors = shouldShow;
-        mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+        setShouldShowSystemDecorsInternalLocked(dc, shouldShow);
 
         if (enableDisplayContentModeManagement()) {
             if (dc.isDefaultDisplay || dc.isPrivate() || !changed) {
@@ -269,6 +264,15 @@
         }
     }
 
+     void setShouldShowSystemDecorsInternalLocked(@NonNull DisplayContent dc,
+            boolean shouldShow) {
+        final DisplayInfo displayInfo = dc.getDisplayInfo();
+        final SettingsProvider.SettingsEntry overrideSettings =
+                mSettingsProvider.getOverrideSettings(displayInfo);
+        overrideSettings.mShouldShowSystemDecors = shouldShow;
+        mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+    }
+
     boolean isHomeSupportedLocked(@NonNull DisplayContent dc) {
         if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
             // Default display should show home.
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index c93efd3..40f16c1 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2775,6 +2775,12 @@
                 return;
             }
 
+            if (enableDisplayContentModeManagement() && display.allowContentModeSwitch()) {
+                mWindowManager.mDisplayWindowSettings
+                        .setShouldShowSystemDecorsInternalLocked(display,
+                                display.mDisplay.canHostTasks());
+            }
+
             startSystemDecorations(display, "displayAdded");
 
             // Drop any cached DisplayInfos associated with this display id - the values are now
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3abab8b..6b3499a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -166,6 +166,7 @@
 import android.view.SurfaceControl;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.window.DesktopExperienceFlags;
 import android.window.DesktopModeFlags;
 import android.window.ITaskOrganizer;
 import android.window.PictureInPictureSurfaceTransaction;
@@ -2027,7 +2028,7 @@
         }
 
         if (shouldStartChangeTransition(prevWinMode, mTmpPrevBounds)) {
-            initializeChangeTransition(mTmpPrevBounds);
+            mTransitionController.collectVisibleChange(this);
         }
 
         // If the configuration supports persistent bounds (eg. Freeform), keep track of the
@@ -2378,7 +2379,7 @@
         // configurations and let its parent (organized task) to control it;
         final Task rootTask = getRootTask();
         boolean shouldInheritBounds = rootTask != this && rootTask.isOrganized();
-        if (Flags.enableMultipleDesktopsBackend()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
             // Only inherit from organized parent when this task is not organized.
             shouldInheritBounds &= !isOrganized();
         }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 74059c1..a698a9e 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2779,9 +2779,7 @@
         }
 
         // If this TaskFragment is closing while resizing, crop to the starting bounds instead.
-        final Rect bounds = isClosingWhenResizing()
-                ? mDisplayContent.mClosingChangingContainers.get(this)
-                : getBounds();
+        final Rect bounds = getBounds();
         final int width = bounds.width();
         final int height = bounds.height();
         if (!forceUpdate && width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index c1ef208..a8b9fed 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -292,12 +292,6 @@
         return false;
     }
 
-    boolean isWallpaperTargetAnimating() {
-        return mWallpaperTarget != null && mWallpaperTarget.isAnimating(TRANSITION | PARENTS)
-                && (mWallpaperTarget.mActivityRecord == null
-                        || !mWallpaperTarget.mActivityRecord.isWaitingForTransitionStart());
-    }
-
     void hideDeferredWallpapersIfNeededLegacy() {
         for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
             final WallpaperWindowToken token = mWallpaperTokens.get(i);
@@ -837,14 +831,6 @@
             // Use the old target if new target is hidden but old target
             // is not. If they're both hidden, still use the new target.
             mWallpaperTarget = prevWallpaperTarget;
-        } else if (newTargetHidden == oldTargetHidden
-                && !mDisplayContent.mOpeningApps.contains(wallpaperTarget.mActivityRecord)
-                && (mDisplayContent.mOpeningApps.contains(prevWallpaperTarget.mActivityRecord)
-                || mDisplayContent.mClosingApps.contains(prevWallpaperTarget.mActivityRecord))) {
-            // If they're both hidden (or both not hidden), prefer the one that's currently in
-            // opening or closing app list, this allows transition selection logic to better
-            // determine the wallpaper status of opening/closing apps.
-            mWallpaperTarget = prevWallpaperTarget;
         }
 
         result.setWallpaperTarget(wallpaperTarget);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 95cdf46..45202a2 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -34,7 +34,6 @@
 import static android.view.SurfaceControl.Transaction;
 import static android.view.WindowInsets.Type.InsetsType;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
-import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
 import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
 
@@ -901,10 +900,6 @@
      */
     @CallSuper
     void removeImmediately() {
-        final DisplayContent dc = getDisplayContent();
-        if (dc != null) {
-            dc.mClosingChangingContainers.remove(this);
-        }
         while (!mChildren.isEmpty()) {
             final E child = mChildren.getLast();
             child.removeImmediately();
@@ -1116,10 +1111,6 @@
             if (asWindowState() == null) {
                 mTransitionController.collect(this);
             }
-            // Cancel any change transition queued-up for this container on the old display when
-            // this container is moved from the old display.
-            mDisplayContent.mClosingChangingContainers.remove(this);
-            mDisplayContent.mChangingContainers.remove(this);
         }
         mDisplayContent = dc;
         if (dc != null && dc != this && mPendingTransaction != null) {
@@ -1268,14 +1259,6 @@
     }
 
     /**
-     * @return {@code true} when the container is waiting the app transition start, {@code false}
-     *         otherwise.
-     */
-    boolean isWaitingForTransitionStart() {
-        return false;
-    }
-
-    /**
      * @return {@code true} if in this subtree of the hierarchy we have an
      *         {@code ActivityRecord#isAnimating(TRANSITION)}, {@code false} otherwise.
      */
@@ -1302,13 +1285,6 @@
         return isAnimating(0 /* self only */);
     }
 
-    /**
-     * @return {@code true} if the container is in changing app transition.
-     */
-    boolean isChangingAppTransition() {
-        return mDisplayContent != null && mDisplayContent.mChangingContainers.contains(this);
-    }
-
     boolean inTransition() {
         return mTransitionController.inTransition(this);
     }
@@ -1427,12 +1403,6 @@
         return setVisibleRequested(newVisReq);
     }
 
-    /** Whether this window is closing while resizing. */
-    boolean isClosingWhenResizing() {
-        return mDisplayContent != null
-                && mDisplayContent.mClosingChangingContainers.containsKey(this);
-    }
-
     void writeIdentifierToProto(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
         proto.write(HASH_CODE, System.identityHashCode(this));
@@ -3044,36 +3014,6 @@
                 || (getParent() != null && getParent().inPinnedWindowingMode());
     }
 
-    /**
-     * Initializes a change transition.
-     *
-     * For now, this will only be called for the following cases:
-     * 1. {@link Task} is changing windowing mode between fullscreen and freeform.
-     * 2. {@link TaskFragment} is organized and is changing window bounds.
-     * 3. {@link ActivityRecord} is reparented into an organized {@link TaskFragment}. (The
-     *    transition will happen on the {@link TaskFragment} for this case).
-     *
-     * This shouldn't be called on other {@link WindowContainer} unless there is a valid
-     * use case.
-     *
-     * @param startBounds The original bounds (on screen) of the surface we are snapshotting.
-     */
-    void initializeChangeTransition(Rect startBounds, @Nullable SurfaceControl freezeTarget) {
-        if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
-            mDisplayContent.mTransitionController.collectVisibleChange(this);
-            return;
-        }
-        mDisplayContent.prepareAppTransition(TRANSIT_CHANGE);
-        mDisplayContent.mChangingContainers.add(this);
-        // Calculate the relative position in parent container.
-        final Rect parentBounds = getParent().getBounds();
-        mTmpPoint.set(startBounds.left - parentBounds.left, startBounds.top - parentBounds.top);
-    }
-
-    void initializeChangeTransition(Rect startBounds) {
-        initializeChangeTransition(startBounds, null /* freezeTarget */);
-    }
-
     ArraySet<WindowContainer> getAnimationSources() {
         return mSurfaceAnimationSources;
     }
@@ -3166,8 +3106,7 @@
         getAnimationPosition(mTmpPoint);
         mTmpRect.offsetTo(0, 0);
 
-        final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter
-                && isChangingAppTransition();
+        final boolean isChanging = AppTransition.isChangeTransitOld(transit);
 
         if (isChanging) {
             final float durationScale = mWmService.getTransitionAnimationScaleLocked();
@@ -3519,9 +3458,6 @@
                 && (mSurfaceAnimator.getAnimationType() & typesToCheck) > 0) {
             return true;
         }
-        if ((flags & TRANSITION) != 0 && isWaitingForTransitionStart()) {
-            return true;
-        }
         return false;
     }
 
@@ -3603,13 +3539,7 @@
             return;
         }
 
-        if (isClosingWhenResizing()) {
-            // This container is closing while resizing, keep its surface at the starting position
-            // to prevent animation flicker.
-            getRelativePosition(mDisplayContent.mClosingChangingContainers.get(this), mTmpPos);
-        } else {
-            getRelativePosition(mTmpPos);
-        }
+        getRelativePosition(mTmpPos);
         final int deltaRotation = getRelativeDisplayRotation();
         if (mTmpPos.equals(mLastSurfacePosition) && deltaRotation == mLastDeltaRotation) {
             return;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 4c53ba5..4b4736e 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1060,7 +1060,7 @@
         effects |= applyChanges(taskFragment, c);
 
         if (taskFragment.shouldStartChangeTransition(mTmpBounds0, mTmpBounds1)) {
-            taskFragment.initializeChangeTransition(mTmpBounds0);
+            mTransitionController.collectVisibleChange(taskFragment);
         }
         taskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
         return effects;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5897241..9f1289b2 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3272,13 +3272,6 @@
                 mDestroying = false;
                 destroyedSomething = true;
             }
-
-            // Since mDestroying will affect ActivityRecord#allDrawn, we need to perform another
-            // traversal in case we are waiting on this window to start the transition.
-            if (getDisplayContent().mAppTransition.isTransitionSet()
-                    && getDisplayContent().mOpeningApps.contains(mActivityRecord)) {
-                mWmService.mWindowPlacerLocked.requestTraversal();
-            }
         }
 
         return destroyedSomething;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index f07e672..0d0c0ba 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -124,6 +124,7 @@
     jmethodID notifyStylusGestureStarted;
     jmethodID notifyVibratorState;
     jmethodID filterInputEvent;
+    jmethodID filterPointerMotion;
     jmethodID interceptKeyBeforeQueueing;
     jmethodID interceptMotionBeforeQueueingNonInteractive;
     jmethodID interceptKeyBeforeDispatching;
@@ -451,6 +452,8 @@
     void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId,
                                        const vec2& position) override;
     void notifyMouseCursorFadedOnTyping() override;
+    std::optional<vec2> filterPointerMotionForAccessibility(
+            const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) override;
 
     /* --- InputFilterPolicyInterface implementation --- */
     void notifyStickyModifierStateChanged(uint32_t modifierState,
@@ -938,6 +941,27 @@
     checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged");
 }
 
+std::optional<vec2> NativeInputManager::filterPointerMotionForAccessibility(
+        const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) {
+    JNIEnv* env = jniEnv();
+    ScopedFloatArrayRO filtered(env,
+                                jfloatArray(
+                                        env->CallObjectMethod(mServiceObj,
+                                                              gServiceClassInfo.filterPointerMotion,
+                                                              delta.x, delta.y, current.x,
+                                                              current.y, displayId.val())));
+    if (checkAndClearExceptionFromCallback(env, "filterPointerMotionForAccessibilityLocked")) {
+        ALOGE("Disabling accessibility pointer motion filter due to an error. "
+              "The filter state in Java and PointerChoreographer would no longer be in sync.");
+        return std::nullopt;
+    }
+    LOG_ALWAYS_FATAL_IF(filtered.size() != 2,
+                        "Accessibility pointer motion filter is misbehaving. Returned array size "
+                        "%zu should be 2.",
+                        filtered.size());
+    return vec2{filtered[0], filtered[1]};
+}
+
 sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(ui::LogicalDisplayId displayId) {
     JNIEnv* env = jniEnv();
     jlong nativeSurfaceControlPtr =
@@ -3271,6 +3295,12 @@
     return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled);
 }
 
+static void nativeSetAccessibilityPointerMotionFilterEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                             jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->getInputManager()->getChoreographer().setAccessibilityPointerMotionFilterEnabled(enabled);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gInputManagerMethods[] = {
@@ -3398,6 +3428,8 @@
         {"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive},
         {"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId},
         {"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled},
+        {"setAccessibilityPointerMotionFilterEnabled", "(Z)V",
+         (void*)nativeSetAccessibilityPointerMotionFilterEnabled},
 };
 
 #define FIND_CLASS(var, className) \
@@ -3482,6 +3514,8 @@
     GET_METHOD_ID(gServiceClassInfo.filterInputEvent, clazz,
             "filterInputEvent", "(Landroid/view/InputEvent;I)Z");
 
+    GET_METHOD_ID(gServiceClassInfo.filterPointerMotion, clazz, "filterPointerMotion", "(FFFFI)[F");
+
     GET_METHOD_ID(gServiceClassInfo.interceptKeyBeforeQueueing, clazz,
             "interceptKeyBeforeQueueing", "(Landroid/view/KeyEvent;I)I");
 
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index a96c477..f731b50 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -17,6 +17,8 @@
 package com.android.server.supervision;
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_USERS;
+import static android.Manifest.permission.QUERY_USERS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
 import static com.android.internal.util.Preconditions.checkCallAuthorization;
@@ -79,6 +81,25 @@
     }
 
     /**
+     * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to
+     * launch the activity to verify supervision credentials.
+     *
+     * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this
+     * method is called, the launched activity still need to perform validity checks as the
+     * supervision state can change when it's launched. A null intent is returned if supervision is
+     * disabled at the time of this method call.
+     *
+     * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification
+     * of the supervision credentials.
+     */
+    @Override
+    @Nullable
+    public Intent createConfirmSupervisionCredentialsIntent() {
+        // TODO(b/392961554): Implement createAuthenticationIntent API
+        throw new UnsupportedOperationException();
+    }
+
+    /**
      * Returns whether supervision is enabled for the given user.
      *
      * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or
@@ -86,6 +107,7 @@
      */
     @Override
     public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+        enforceAnyPermission(QUERY_USERS, MANAGE_USERS);
         if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
             enforcePermission(INTERACT_ACROSS_USERS);
         }
@@ -96,6 +118,7 @@
 
     @Override
     public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+        // TODO(b/395630828): Ensure that this method can only be called by the system.
         if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
             enforcePermission(INTERACT_ACROSS_USERS);
         }
@@ -181,8 +204,8 @@
      * Ensures that supervision is enabled when the supervision app is the profile owner.
      *
      * <p>The state syncing with the DevicePolicyManager can only enable supervision and never
-     * disable. Supervision can only be disabled explicitly via calls to the
-     * {@link #setSupervisionEnabledForUser} method.
+     * disable. Supervision can only be disabled explicitly via calls to the {@link
+     * #setSupervisionEnabledForUser} method.
      */
     private void syncStateWithDevicePolicyManager(@UserIdInt int userId) {
         final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal();
@@ -221,6 +244,17 @@
                 mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED);
     }
 
+    /** Enforces that the caller has at least one of the given permission. */
+    private void enforceAnyPermission(String... permissions) {
+        boolean authorized = false;
+        for (String permission : permissions) {
+            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+                authorized = true;
+            }
+        }
+        checkCallAuthorization(authorized);
+    }
+
     /** Provides local services in a lazy manner. */
     static class Injector {
         private final Context mContext;
@@ -280,7 +314,7 @@
         }
 
         @VisibleForTesting
-        @SuppressLint("MissingPermission")  // not needed for a system service
+        @SuppressLint("MissingPermission")
         void registerProfileOwnerListener() {
             IntentFilter poIntentFilter = new IntentFilter();
             poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 5d64cb6..2d3f723 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -34,12 +34,12 @@
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.content.res.Configuration;
 import android.graphics.Insets;
+import android.os.Build;
 import android.os.RemoteException;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.server.wm.WindowManagerStateHelper;
@@ -86,25 +86,36 @@
             "android:id/input_method_nav_back";
     private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID =
             "android:id/input_method_nav_ime_switcher";
-    private static final long TIMEOUT_IN_SECONDS = 3;
-    private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
-            "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
-    private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
-            "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
+
+    /** Timeout until the uiObject should be found. */
+    private static final long TIMEOUT_MS = 5000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+    /** Timeout until the event is expected. */
+    private static final long EXPECT_TIMEOUT_MS = 3000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+    /** Timeout during which the event is not expected. */
+    private static final long NOT_EXCEPT_TIMEOUT_MS = 2000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+    /** Command to set showing the IME when a hardware keyboard is connected. */
+    private static final String SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
+            "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD;
+    /** Command to get verbose ImeTracker logging state. */
+    private static final String GET_VERBOSE_IME_TRACKER_LOGGING_CMD =
+            "getprop persist.debug.imetracker";
+    /** Command to set verbose ImeTracker logging state. */
+    private static final String SET_VERBOSE_IME_TRACKER_LOGGING_CMD =
+            "setprop persist.debug.imetracker";
 
     /** The ids of the subtypes of SimpleIme. */
     private static final int[] SUBTYPE_IDS = new int[]{1, 2};
 
-    private final WindowManagerStateHelper mWmState =  new WindowManagerStateHelper();
+    private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
 
     private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper();
 
     private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
 
     @Rule
-    public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
-
-    @Rule
     public final TestName mName = new TestName();
 
     private Instrumentation mInstrumentation;
@@ -114,7 +125,8 @@
     private String mInputMethodId;
     private TestActivity mActivity;
     private InputMethodServiceWrapper mInputMethodService;
-    private boolean mShowImeWithHardKeyboardEnabled;
+    private boolean mOriginalVerboseImeTrackerLoggingEnabled;
+    private boolean mOriginalShowImeWithHardKeyboardEnabled;
 
     @Before
     public void setUp() throws Exception {
@@ -123,9 +135,12 @@
         mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class);
         mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
         mInputMethodId = getInputMethodId();
+        mOriginalVerboseImeTrackerLoggingEnabled = getVerboseImeTrackerLogging();
+        if (!mOriginalVerboseImeTrackerLoggingEnabled) {
+            setVerboseImeTrackerLogging(true);
+        }
         prepareIme();
         prepareActivity();
-        mInstrumentation.waitForIdleSync();
         mUiDevice.freezeRotation();
         mUiDevice.setOrientationNatural();
         // Waits for input binding ready.
@@ -148,17 +163,18 @@
                     .that(mInputMethodService.getCurrentInputViewStarted()).isFalse();
         });
         // Save the original value of show_ime_with_hard_keyboard from Settings.
-        mShowImeWithHardKeyboardEnabled =
+        mOriginalShowImeWithHardKeyboardEnabled =
                 mInputMethodService.getShouldShowImeWithHardKeyboardForTesting();
     }
 
     @After
     public void tearDown() throws Exception {
         mUiDevice.unfreezeRotation();
+        if (!mOriginalVerboseImeTrackerLoggingEnabled) {
+            setVerboseImeTrackerLogging(false);
+        }
         // Change back the original value of show_ime_with_hard_keyboard in Settings.
-        executeShellCommand(mShowImeWithHardKeyboardEnabled
-                ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
-                : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+        setShowImeWithHardKeyboard(mOriginalShowImeWithHardKeyboardEnabled);
         executeShellCommand("ime disable " + mInputMethodId);
     }
 
@@ -170,7 +186,7 @@
     public void testShowHideKeyboard_byUserAction() {
         waitUntilActivityReadyForInputInjection(mActivity);
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         // Performs click on EditText to bring up the IME.
         Log.i(TAG, "Click on EditText");
@@ -201,14 +217,12 @@
      */
     @Test
     public void testShowHideKeyboard_byInputMethodManager() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
-        // Triggers to show IME via public API.
         verifyInputViewStatusOnMainSync(
                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
                 EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
 
-        // Triggers to hide IME via public API.
         verifyInputViewStatusOnMainSync(
                 () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
                 EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
@@ -219,14 +233,12 @@
      */
     @Test
     public void testShowHideKeyboard_byInsetsController() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
-        // Triggers to show IME via public API.
         verifyInputViewStatusOnMainSync(
                 () -> mActivity.showImeWithWindowInsetsController(),
                 EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
 
-        // Triggers to hide IME via public API.
         verifyInputViewStatusOnMainSync(
                 () -> mActivity.hideImeWithWindowInsetsController(),
                 EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
@@ -234,53 +246,18 @@
 
     /**
      * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
-     *
-     * <p>With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked
-     * will be just apply the requested visibility (by using the callback). Therefore, we will
-     * lose flags like HIDE_IMPLICIT_ONLY.
      */
     @Test
     public void testShowHideSelf() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
-        // IME request to show itself without any flags, expect shown.
-        Log.i(TAG, "Call IMS#requestShowSelf(0)");
         verifyInputViewStatusOnMainSync(
                 () -> mInputMethodService.requestShowSelf(0 /* flags */),
                 EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
 
-        if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
-            // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
-            Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
-            verifyInputViewStatusOnMainSync(
-                    () -> mInputMethodService.requestHideSelf(
-                            InputMethodManager.HIDE_IMPLICIT_ONLY),
-                    EVENT_HIDE, false /* eventExpected */, true /* shown */,
-                    "IME is still shown after HIDE_IMPLICIT_ONLY");
-        }
-
-        // IME request to hide itself without any flags, expect hidden.
-        Log.i(TAG, "Call IMS#requestHideSelf(0)");
         verifyInputViewStatusOnMainSync(
                 () -> mInputMethodService.requestHideSelf(0 /* flags */),
                 EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
-
-        if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
-            // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
-            Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
-            verifyInputViewStatusOnMainSync(
-                    () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
-                    EVENT_SHOW, true /* eventExpected */, true /* shown */,
-                    "IME is shown with SHOW_IMPLICIT");
-
-            // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
-            Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
-            verifyInputViewStatusOnMainSync(
-                    () -> mInputMethodService.requestHideSelf(
-                            InputMethodManager.HIDE_IMPLICIT_ONLY),
-                    EVENT_HIDE, true /* eventExpected */, false /* shown */,
-                    "IME is not shown after HIDE_IMPLICIT_ONLY");
-        }
     }
 
     /**
@@ -289,28 +266,25 @@
      */
     @Test
     public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
         try {
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
-            eventually(() ->
-                    assertWithMessage("InputView should show with visible hardware keyboard")
-                            .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+            assertWithMessage("InputView should show with visible hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
 
             config.keyboard = Configuration.KEYBOARD_NOKEYS;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
-            eventually(() ->
-                    assertWithMessage("InputView should show without hardware keyboard")
-                            .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+            assertWithMessage("InputView should show without hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
 
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
-            eventually(() ->
-                    assertWithMessage("InputView should show with hidden hardware keyboard")
-                         .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+            assertWithMessage("InputView should show with hidden hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
         } finally {
             mInputMethodService.getResources()
                     .updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -323,28 +297,25 @@
      */
     @Test
     public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
         try {
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
-            eventually(() ->
-                    assertWithMessage("InputView should not show with visible hardware keyboard")
-                            .that(mInputMethodService.onEvaluateInputViewShown()).isFalse());
+            assertWithMessage("InputView should not show with visible hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isFalse();
 
             config.keyboard = Configuration.KEYBOARD_NOKEYS;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
-            eventually(() ->
-                    assertWithMessage("InputView should show without hardware keyboard")
-                            .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+            assertWithMessage("InputView should show without hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
 
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
-            eventually(() ->
-                    assertWithMessage("InputView should show with hidden hardware keyboard")
-                            .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+            assertWithMessage("InputView should show with hidden hardware keyboard")
+                    .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
         } finally {
             mInputMethodService.getResources()
                     .updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -357,7 +328,7 @@
      */
     @Test
     public void testShowSoftInput_disableShowImeWithHardKeyboard() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -386,49 +357,17 @@
     }
 
     /**
-     * This checks that an explicit show request results in the IME being shown.
-     */
-    @Test
-    public void testShowSoftInputExplicitly() {
-        setShowImeWithHardKeyboard(true /* enabled */);
-
-        // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
-        // IME should be shown.
-        verifyInputViewStatusOnMainSync(
-                () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
-                EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
-    }
-
-    /**
-     * This checks that an implicit show request results in the IME being shown.
-     */
-    @Test
-    public void testShowSoftInputImplicitly() {
-        setShowImeWithHardKeyboard(true /* enabled */);
-
-        // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
-        // the IME should be shown.
-        verifyInputViewStatusOnMainSync(() -> assertThat(
-                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
-                EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
-    }
-
-    /**
      * This checks that an explicit show request when the IME is not previously shown,
      * and it should be shown in fullscreen mode, results in the IME being shown.
      */
     @Test
     public void testShowSoftInputExplicitly_fullScreenMode() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         // Set orientation landscape to enable fullscreen mode.
         setOrientation(2);
-        eventually(() -> assertWithMessage("No longer in natural orientation")
-                .that(mUiDevice.isNaturalOrientation()).isFalse());
-        // Wait for the TestActivity to be recreated.
         eventually(() -> assertWithMessage("Activity was re-created after rotation")
                 .that(TestActivity.getInstance()).isNotEqualTo(mActivity));
-        // Get the new TestActivity.
         mActivity = TestActivity.getInstance();
         assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
         // Wait for the new EditText to be served by InputMethodManager.
@@ -442,34 +381,40 @@
 
     /**
      * This checks that an implicit show request when the IME is not previously shown,
-     * and it should be shown in fullscreen mode, results in the IME not being shown.
+     * and it should be shown in fullscreen mode behaves like an explicit show request, resulting
+     * in the IME being shown. This is due to the refactor in b/298172246, causing us to lose flag
+     * information like {@link InputMethodManager#SHOW_IMPLICIT}.
      *
-     * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
-     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
-     * SHOW_IMPLICIT.
+     * <p>Previously, an implicit show request when the IME is not previously shown,
+     * and it should be shown in fullscreen mode, would result in the IME not being shown.
      */
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_fullScreenMode() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         // Set orientation landscape to enable fullscreen mode.
         setOrientation(2);
-        eventually(() -> assertWithMessage("No longer in natural orientation")
-                .that(mUiDevice.isNaturalOrientation()).isFalse());
-        // Wait for the TestActivity to be recreated.
         eventually(() -> assertWithMessage("Activity was re-created after rotation")
                 .that(TestActivity.getInstance()).isNotEqualTo(mActivity));
-        // Get the new TestActivity.
         mActivity = TestActivity.getInstance();
         assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
         // Wait for the new EditText to be served by InputMethodManager.
         eventually(() -> assertWithMessage("Has an input connection to the re-created Activity")
                 .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
 
-        verifyInputViewStatusOnMainSync(() -> assertThat(
-                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
-                EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+            verifyInputViewStatusOnMainSync(() -> assertThat(
+                            mActivity.showImeWithInputMethodManager(
+                                    InputMethodManager.SHOW_IMPLICIT))
+                            .isTrue(),
+                    EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+        } else {
+            verifyInputViewStatusOnMainSync(() -> assertThat(
+                            mActivity.showImeWithInputMethodManager(
+                                    InputMethodManager.SHOW_IMPLICIT))
+                            .isTrue(),
+                    EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+        }
     }
 
     /**
@@ -478,7 +423,7 @@
      */
     @Test
     public void testShowSoftInputExplicitly_withHardKeyboard() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -497,17 +442,17 @@
     }
 
     /**
-     * This checks that an implicit show request when a hardware keyboard is connected,
-     * results in the IME not being shown.
+     * This checks that an implicit show request when a hardware keyboard is connected behaves
+     * like an explicit show request, resulting in the IME being shown. This is due to the
+     * refactor in b/298172246, causing us to lose flag information like
+     * {@link InputMethodManager#SHOW_IMPLICIT}.
      *
-     * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
-     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
-     * SHOW_IMPLICIT.
+     * <p>Previously, an implicit show request when a hardware keyboard is connected would
+     * result in the IME not being shown.
      */
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_withHardKeyboard() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -516,10 +461,20 @@
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
 
-            verifyInputViewStatusOnMainSync(() ->assertThat(
-                    mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT))
-                            .isTrue(),
-                    EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+            if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+                verifyInputViewStatusOnMainSync(() -> assertThat(
+                                mActivity.showImeWithInputMethodManager(
+                                        InputMethodManager.SHOW_IMPLICIT))
+                                .isTrue(),
+                        EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+            } else {
+                verifyInputViewStatusOnMainSync(() -> assertThat(
+                                mActivity.showImeWithInputMethodManager(
+                                        InputMethodManager.SHOW_IMPLICIT))
+                                .isTrue(),
+                        EVENT_SHOW, false /* eventExpected */, false /* shown */,
+                        "IME is not shown");
+            }
         } finally {
             mInputMethodService.getResources()
                     .updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -532,7 +487,7 @@
      */
     @Test
     public void testShowSoftInputExplicitly_thenConfigurationChanged() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -565,17 +520,17 @@
 
     /**
      * This checks that an implicit show request followed by connecting a hardware keyboard
-     * and a configuration change, does not trigger IMS#onFinishInputView,
-     * but results in the IME being hidden.
+     * and a configuration change behaves like an explicit show request, resulting in the IME
+     * still being shown. This is due to the refactor in b/298172246, causing us to lose flag
+     * information like {@link InputMethodManager#SHOW_IMPLICIT}.
      *
-     * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
-     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
-     * SHOW_IMPLICIT.
+     * <p>Previously, an implicit show request followed by connecting a hardware keyboard
+     * and a configuration change, would not trigger IMS#onFinishInputView, but resulted in the
+     * IME being hidden.
      */
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_thenConfigurationChanged() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -596,16 +551,23 @@
             // Simulate a fake configuration change to avoid the recreation of TestActivity.
             config.orientation = Configuration.ORIENTATION_LANDSCAPE;
 
-            // Normally, IMS#onFinishInputView will be called when finishing the input view by
-            // the user. But if IMS#hideWindow is called when receiving a new configuration change,
-            // we don't expect that it's user-driven to finish the lifecycle of input view with
-            // IMS#onFinishInputView, because the input view will be re-initialized according
-            // to the last #mShowInputRequested state. So in this case we treat the input view as
-            // still alive.
-            verifyInputViewStatusOnMainSync(
-                    () -> mInputMethodService.onConfigurationChanged(config),
-                    EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */,
-                    false /* shown */, "IME is not shown after a configuration change");
+            if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+                verifyInputViewStatusOnMainSync(
+                        () -> mInputMethodService.onConfigurationChanged(config),
+                        EVENT_CONFIG, true /* eventExpected */, true /* shown */,
+                        "IME is still shown after a configuration change");
+            } else {
+                // Normally, IMS#onFinishInputView will be called when finishing the input view by
+                // the user. But if IMS#hideWindow is called when receiving a new configuration
+                // change, we don't expect that it's user-driven to finish the lifecycle of input
+                // view with IMS#onFinishInputView, because the input view will be re-initialized
+                // according to the last #mShowInputRequested state. So in this case we treat the
+                // input view as still alive.
+                verifyInputViewStatusOnMainSync(
+                        () -> mInputMethodService.onConfigurationChanged(config),
+                        EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */,
+                        false /* shown */, "IME is not shown after a configuration change");
+            }
         } finally {
             mInputMethodService.getResources()
                     .updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -619,7 +581,7 @@
      */
     @Test
     public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() {
-        setShowImeWithHardKeyboard(false /* enabled */);
+        setShowImeWithHardKeyboard(false /* enable */);
 
         final var config = mInputMethodService.getResources().getConfiguration();
         final var initialConfig = new Configuration(config);
@@ -628,12 +590,10 @@
             config.keyboard = Configuration.KEYBOARD_QWERTY;
             config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
 
-            // Explicit show request.
             verifyInputViewStatusOnMainSync(() -> assertThat(
                             mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
                     EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
 
-            // Implicit show request.
             verifyInputViewStatusOnMainSync(() -> assertThat(
                             mActivity.showImeWithInputMethodManager(
                                     InputMethodManager.SHOW_IMPLICIT)).isTrue(),
@@ -654,17 +614,18 @@
 
     /**
      * This checks that a forced show request directly followed by an explicit show request,
-     * and then a hide not always request, still results in the IME being shown
-     * (i.e. the explicit show request retains the forced state).
+     * and then a not always hide request behaves like a normal hide request, resulting in the
+     * IME being hidden (i.e. the explicit show request does not retain the forced state). This is
+     * due to the refactor in b/298172246, causing us to lose flag information like
+     * {@link InputMethodManager#SHOW_FORCED}.
      *
-     * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
-     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
-     * HIDE_NOT_ALWAYS.
+     * <p>Previously, a forced show request directly followed by an explicit show request,
+     * and then a not always hide request, would result in the IME still being shown
+     * (i.e. the explicit show request would retain the forced state).
      */
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         verifyInputViewStatusOnMainSync(() -> assertThat(
                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
@@ -674,11 +635,123 @@
                         mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
                 EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown");
 
-        verifyInputViewStatusOnMainSync(() -> assertThat(
-                        mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
-                        .isTrue(),
-                EVENT_HIDE, false /* eventExpected */, true /* shown */,
-                "IME is still shown after HIDE_NOT_ALWAYS");
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+            verifyInputViewStatusOnMainSync(() -> assertThat(mActivity
+                            .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
+                            .isTrue(),
+                    EVENT_HIDE, true /* eventExpected */, false /* shown */,
+                    "IME is not shown after HIDE_NOT_ALWAYS");
+        } else {
+            verifyInputViewStatusOnMainSync(() -> assertThat(mActivity
+                            .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
+                            .isTrue(),
+                    EVENT_HIDE, false /* eventExpected */, true /* shown */,
+                    "IME is still shown after HIDE_NOT_ALWAYS");
+        }
+    }
+
+    /**
+     * This checks that an explicit show request followed by an implicit only hide request
+     * behaves like a normal hide request, resulting in the IME being hidden. This is due to
+     * the refactor in b/298172246, causing us to lose flag information like
+     * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}.
+     *
+     * <p>Previously, an explicit show request followed by an implicit only hide request
+     * would result in the IME still being shown.
+     */
+    @Test
+    public void testShowSoftInputExplicitly_thenHideSoftInputImplicitOnly() {
+        setShowImeWithHardKeyboard(true /* enable */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> mActivity.showImeWithInputMethodManager(0 /* flags */),
+                EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+            verifyInputViewStatusOnMainSync(
+                    () -> mActivity.hideImeWithInputMethodManager(
+                            InputMethodManager.HIDE_IMPLICIT_ONLY),
+                    EVENT_HIDE, true /* eventExpected */, false /* shown */,
+                    "IME is not shown after HIDE_IMPLICIT_ONLY");
+        } else {
+            verifyInputViewStatusOnMainSync(
+                    () -> mActivity.hideImeWithInputMethodManager(
+                            InputMethodManager.HIDE_IMPLICIT_ONLY),
+                    EVENT_HIDE, false /* eventExpected */, true /* shown */,
+                    "IME is still shown after HIDE_IMPLICIT_ONLY");
+        }
+    }
+
+    /**
+     * This checks that an implicit show request followed by an implicit only hide request
+     * results in the IME being hidden.
+     */
+    @Test
+    public void testShowSoftInputImplicitly_thenHideSoftInputImplicitOnly() {
+        setShowImeWithHardKeyboard(true /* enable */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT),
+                EVENT_SHOW, true /* eventExpected */, true /* shown */,
+                "IME is shown with SHOW_IMPLICIT");
+
+        verifyInputViewStatusOnMainSync(
+                () -> mActivity.hideImeWithInputMethodManager(
+                        InputMethodManager.HIDE_IMPLICIT_ONLY),
+                EVENT_HIDE, true /* eventExpected */, false /* shown */,
+                "IME is not shown after HIDE_IMPLICIT_ONLY");
+    }
+
+    /**
+     * This checks that an explicit show self request followed by an implicit only hide self request
+     * behaves like a normal hide self request, resulting in the IME being hidden. This is due to
+     * the refactor in b/298172246, causing us to lose flag information like
+     * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}.
+     *
+     * <p>Previously, an explicit show self request followed by an implicit only hide self request
+     * would result in the IME still being shown.
+     */
+    @Test
+    public void testShowSelfExplicitly_thenHideSelfImplicitOnly() {
+        setShowImeWithHardKeyboard(true /* enable */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestShowSelf(0 /* flags */),
+                EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+            verifyInputViewStatusOnMainSync(
+                    () -> mInputMethodService.requestHideSelf(
+                            InputMethodManager.HIDE_IMPLICIT_ONLY),
+                    EVENT_HIDE, true /* eventExpected */, false /* shown */,
+                    "IME is not shown after HIDE_IMPLICIT_ONLY");
+        } else {
+            verifyInputViewStatusOnMainSync(
+                    () -> mInputMethodService.requestHideSelf(
+                            InputMethodManager.HIDE_IMPLICIT_ONLY),
+                    EVENT_HIDE, false /* eventExpected */, true /* shown */,
+                    "IME is still shown after HIDE_IMPLICIT_ONLY");
+        }
+    }
+
+    /**
+     * This checks that an implicit show self request followed by an implicit only hide self request
+     * results in the IME being hidden.
+     */
+    @Test
+    public void testShowSelfImplicitly_thenHideSelfImplicitOnly() {
+        setShowImeWithHardKeyboard(true /* enable */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+                EVENT_SHOW, true /* eventExpected */, true /* shown */,
+                "IME is shown with SHOW_IMPLICIT");
+
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestHideSelf(
+                        InputMethodManager.HIDE_IMPLICIT_ONLY),
+                EVENT_HIDE, true /* eventExpected */, false /* shown */,
+                "IME is not shown after HIDE_IMPLICIT_ONLY");
     }
 
     /**
@@ -686,7 +759,7 @@
      */
     @Test
     public void testFullScreenMode() {
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         Log.i(TAG, "Set orientation natural");
         verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */,
@@ -723,25 +796,22 @@
     public void testShowHideImeNavigationBar_doesDrawImeNavBar() {
         assumeTrue("Must have a navigation bar", hasNavigationBar());
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
-        // Show IME
         verifyInputViewStatusOnMainSync(
                 () -> {
-                    setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+                    setDrawsImeNavBarAndSwitcherButton(true /* enable */);
                     mActivity.showImeWithWindowInsetsController();
                 },
                 EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
         assertWithMessage("IME navigation bar is initially shown")
                 .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
 
-        // Try to hide IME nav bar
         mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */));
         mInstrumentation.waitForIdleSync();
         assertWithMessage("IME navigation bar is not shown after hide request")
                 .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
 
-        // Try to show IME nav bar
         mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */));
         mInstrumentation.waitForIdleSync();
         assertWithMessage("IME navigation bar is shown after show request")
@@ -758,25 +828,22 @@
     public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() {
         assumeTrue("Must have a navigation bar", hasNavigationBar());
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
-        // Show IME
         verifyInputViewStatusOnMainSync(
                 () -> {
-                    setDrawsImeNavBarAndSwitcherButton(false /* enabled */);
+                    setDrawsImeNavBarAndSwitcherButton(false /* enable */);
                     mActivity.showImeWithWindowInsetsController();
                 },
                 EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
         assertWithMessage("IME navigation bar is initially not shown")
                 .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
 
-        // Try to hide IME nav bar
         mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */));
         mInstrumentation.waitForIdleSync();
         assertWithMessage("IME navigation bar is not shown after hide request")
                 .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
 
-        // Try to show IME nav bar
         mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */));
         mInstrumentation.waitForIdleSync();
         assertWithMessage("IME navigation bar is not shown after show request")
@@ -792,7 +859,7 @@
 
         waitUntilActivityReadyForInputInjection(mActivity);
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
             verifyInputViewStatusOnMainSync(
@@ -818,7 +885,7 @@
 
         waitUntilActivityReadyForInputInjection(mActivity);
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
             verifyInputViewStatusOnMainSync(
@@ -844,7 +911,7 @@
 
         waitUntilActivityReadyForInputInjection(mActivity);
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         final var info = mImm.getCurrentInputMethodInfo();
         assertWithMessage("InputMethodInfo is not null").that(info).isNotNull();
@@ -855,7 +922,7 @@
         try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
             verifyInputViewStatusOnMainSync(
                     () -> {
-                        setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+                        setDrawsImeNavBarAndSwitcherButton(true /* enable */);
                         mActivity.showImeWithWindowInsetsController();
                     },
                     EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
@@ -884,7 +951,7 @@
 
         waitUntilActivityReadyForInputInjection(mActivity);
 
-        setShowImeWithHardKeyboard(true /* enabled */);
+        setShowImeWithHardKeyboard(true /* enable */);
 
         final var info = mImm.getCurrentInputMethodInfo();
         assertWithMessage("InputMethodInfo is not null").that(info).isNotNull();
@@ -893,7 +960,7 @@
         try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
             verifyInputViewStatusOnMainSync(
                     () -> {
-                        setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+                        setDrawsImeNavBarAndSwitcherButton(true /* enable */);
                         mActivity.showImeWithWindowInsetsController();
                     },
                     EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
@@ -956,7 +1023,8 @@
                 runnable.run();
             }
             mInstrumentation.waitForIdleSync();
-            eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+            eventCalled = latch.await(eventExpected ? EXPECT_TIMEOUT_MS : NOT_EXCEPT_TIMEOUT_MS,
+                    TimeUnit.MILLISECONDS);
         } catch (InterruptedException e) {
             fail("Interrupted while waiting for latch: " + e.getMessage());
             return;
@@ -1016,10 +1084,8 @@
         verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */,
                 "IME is not shown");
         if (eventExpected) {
-            // Wait for the TestActivity to be recreated.
             eventually(() -> assertWithMessage("Activity was re-created after rotation")
                     .that(TestActivity.getInstance()).isNotEqualTo(mActivity));
-            // Get the new TestActivity.
             mActivity = TestActivity.getInstance();
             assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
             // Wait for the new EditText to be served by InputMethodManager.
@@ -1062,6 +1128,7 @@
 
     private void prepareActivity() {
         mActivity = TestActivity.startSync(mInstrumentation);
+        mInstrumentation.waitForIdleSync();
         Log.i(TAG, "Finish preparing activity with editor.");
     }
 
@@ -1086,21 +1153,51 @@
      * @param enable the value to be set.
      */
     private void setShowImeWithHardKeyboard(boolean enable) {
+        if (mInputMethodService == null) {
+            // If the IME is no longer around, reset the setting unconditionally.
+            executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0"));
+            return;
+        }
+
         final boolean currentEnabled =
                 mInputMethodService.getShouldShowImeWithHardKeyboardForTesting();
         if (currentEnabled != enable) {
-            executeShellCommand(enable
-                    ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
-                    : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+            executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0"));
             eventually(() -> assertWithMessage("showImeWithHardKeyboard updated")
                     .that(mInputMethodService.getShouldShowImeWithHardKeyboardForTesting())
                     .isEqualTo(enable));
         }
     }
 
-    private static void executeShellCommand(@NonNull String cmd) {
+    /**
+     * Gets the verbose logging state in {@link android.view.inputmethod.ImeTracker}.
+     *
+     * @return {@code true} iff verbose logging is enabled.
+     */
+    private static boolean getVerboseImeTrackerLogging() {
+        return executeShellCommand(GET_VERBOSE_IME_TRACKER_LOGGING_CMD).trim().equals("1");
+    }
+
+    /**
+     * Sets verbose logging in {@link android.view.inputmethod.ImeTracker}.
+     *
+     * @param enabled whether to enable or disable verbose logging.
+     *
+     * @implNote This must use {@link ActivityManager#notifySystemPropertiesChanged()} to listen
+     *           for changes to the system property for the verbose ImeTracker logging.
+     */
+    private void setVerboseImeTrackerLogging(boolean enabled) {
+        final var context = mInstrumentation.getContext();
+        final var am = context.getSystemService(ActivityManager.class);
+
+        executeShellCommand(SET_VERBOSE_IME_TRACKER_LOGGING_CMD + " " + (enabled ? "1" : "0"));
+        am.notifySystemPropertiesChanged();
+    }
+
+    @NonNull
+    private static String executeShellCommand(@NonNull String cmd) {
         Log.i(TAG, "Run command: " + cmd);
-        SystemUtil.runShellCommandOrThrow(cmd);
+        return SystemUtil.runShellCommandOrThrow(cmd);
     }
 
     /**
@@ -1113,8 +1210,7 @@
 
     @NonNull
     private UiObject2 getUiObject(@NonNull BySelector bySelector) {
-        final var uiObject = mUiDevice.wait(Until.findObject(bySelector),
-                TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+        final var uiObject = mUiDevice.wait(Until.findObject(bySelector), TIMEOUT_MS);
         assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull();
         return uiObject;
     }
@@ -1137,10 +1233,10 @@
      *
      * <p>Note, neither of these are normally drawn when in three button navigation mode.
      *
-     * @param enabled whether the IME nav bar and IME Switcher button are drawn.
+     * @param enable whether the IME nav bar and IME Switcher button are drawn.
      */
-    private void setDrawsImeNavBarAndSwitcherButton(boolean enabled) {
-        final int flags = enabled ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0;
+    private void setDrawsImeNavBarAndSwitcherButton(boolean enable) {
+        final int flags = enable ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0;
         mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(flags);
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
index 558d1a7..d4d4dca 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
@@ -111,12 +111,6 @@
     }
 
     @Override
-    public void requestHideSelf(int flags) {
-        Log.i(TAG, "requestHideSelf() " + flags);
-        super.requestHideSelf(flags);
-    }
-
-    @Override
     public void onConfigurationChanged(Configuration newConfig) {
         Log.i(TAG, "onConfigurationChanged() " + newConfig);
         super.onConfigurationChanged(newConfig);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 7d3cd8a..38de7ce 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -34,6 +34,7 @@
 import static com.android.server.display.DisplayDeviceInfo.DIFF_EVERYTHING;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
+import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
@@ -1180,13 +1181,21 @@
         assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED,
                 mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
 
+        // Change the display committed state
+        when(mFlagsMock.isCommittedStateSeparateEventEnabled()).thenReturn(true);
+        newDisplayInfo = new DisplayInfo();
+        newDisplayInfo.committedState = STATE_OFF;
+        assertEquals(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED,
+                mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
 
         // Change multiple properties
         newDisplayInfo = new DisplayInfo();
         newDisplayInfo.refreshRateOverride = 30;
         newDisplayInfo.state = STATE_OFF;
+        newDisplayInfo.committedState = STATE_OFF;
         assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED
-                        | LOGICAL_DISPLAY_EVENT_STATE_CHANGED,
+                        | LOGICAL_DISPLAY_EVENT_STATE_CHANGED
+                        | LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED,
                 mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index db04d39e..6c73f0a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -47,7 +47,6 @@
 import android.os.RecoverySystem;
 import android.os.SystemProperties;
 import android.os.UserHandle;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
@@ -135,8 +134,7 @@
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getFlags() {
         return FlagsParameterization.allCombinationsOf(
-                Flags.FLAG_RECOVERABILITY_DETECTION,
-                Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS);
+                Flags.FLAG_RECOVERABILITY_DETECTION);
     }
 
     public RescuePartyTest(FlagsParameterization flags) {
@@ -248,7 +246,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testBootLoopNoFlags() {
         // this is old test where the flag needs to be disabled
         noteBoot(1);
@@ -260,7 +257,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testPersistentAppCrashNoFlags() {
         // this is old test where the flag needs to be disabled
         noteAppCrash(1, true);
@@ -396,7 +392,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testHealthCheckLevelsNoFlags() {
         // this is old test where the flag needs to be disabled
         RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
@@ -416,7 +411,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testBootLoopLevelsNoFlags() {
         RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
 
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 83a390d..4e56422 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -437,6 +437,42 @@
     }
 
     @Test
+    public void testOnGroupChanged_perDisplayWakeByTouchEnabled() {
+        createNotifier();
+        // GIVEN per-display wake by touch is enabled and one display group has been defined with
+        // two displays
+        when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(true);
+        final int groupId = 121;
+        final int displayId1 = 1221;
+        final int displayId2 = 1222;
+        final int[] displays = new int[]{displayId1, displayId2};
+        when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displays));
+        when(mDisplayManagerInternal.getDisplayIdsForGroup(groupId)).thenReturn(displays);
+        SparseArray<int[]> displayIdsByGroupId = new SparseArray<>();
+        displayIdsByGroupId.put(groupId, displays);
+        when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(displayIdsByGroupId);
+        mNotifier.onGroupWakefulnessChangeStarted(
+                groupId, WAKEFULNESS_AWAKE, PowerManager.WAKE_REASON_TAP, /* eventTime= */ 1000);
+        final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+        expectedDisplayInteractivities.put(displayId1, true);
+        expectedDisplayInteractivities.put(displayId2, true);
+        verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+
+        // WHEN display group is changed to only contain one display
+        SparseArray<int[]> newDisplayIdsByGroupId = new SparseArray<>();
+        newDisplayIdsByGroupId.put(groupId, new int[]{displayId1});
+        when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(newDisplayIdsByGroupId);
+        mNotifier.onGroupChanged();
+
+        // THEN native input manager is informed that the displays in the group have changed
+        final SparseBooleanArray expectedDisplayInteractivitiesAfterChange =
+            new SparseBooleanArray();
+        expectedDisplayInteractivitiesAfterChange.put(displayId1, true);
+        verify(mInputManagerInternal).setDisplayInteractivities(
+            expectedDisplayInteractivitiesAfterChange);
+    }
+
+    @Test
     public void testOnWakeLockReleased_FrameworkStatsLogged_NoChains() {
         when(mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()).thenReturn(true);
         createNotifier();
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index 29a17e1..ff67965 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -2501,6 +2501,49 @@
     }
 
     @Test
+    public void testMultiDisplay_twoDisplays_onlyDefaultDisplayCanDream() {
+        final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
+        final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1;
+        final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener =
+                new AtomicReference<>();
+        doAnswer((Answer<Void>) invocation -> {
+            listener.set(invocation.getArgument(0));
+            return null;
+        }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any());
+        final DisplayInfo info = new DisplayInfo();
+        info.displayGroupId = nonDefaultDisplayGroupId;
+        when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info);
+        when(mBatteryManagerInternalMock.isPowered(anyInt())).thenReturn(true);
+        Settings.Secure.putInt(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+        doAnswer(inv -> {
+            when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+            return null;
+        }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+        setMinimumScreenOffTimeoutConfig(5);
+        createService();
+        startSystem();
+
+        listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
+
+        assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
+                WAKEFULNESS_AWAKE);
+        assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
+                WAKEFULNESS_AWAKE);
+        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
+
+        advanceTime(15000);
+
+        // Only the default display group is dreaming.
+        assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
+                WAKEFULNESS_DREAMING);
+        assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
+                WAKEFULNESS_DOZING);
+        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+    }
+
+    @Test
     public void testMultiDisplay_addNewDisplay_becomeGloballyAwakeButDefaultRemainsDozing() {
         final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
         final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1;
diff --git a/services/tests/powerstatstests/res/raw/battery-history.zip b/services/tests/powerstatstests/res/raw/battery-history.zip
new file mode 100644
index 0000000..ed82ac0
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/battery-history.zip
Binary files differ
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java
new file mode 100644
index 0000000..8fc8c9f
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2025 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.power.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.BatteryConsumer;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.ConditionVariable;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import com.android.internal.os.Clock;
+import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.CpuScalingPolicyReader;
+import com.android.internal.os.MonotonicClock;
+import com.android.internal.os.PowerProfile;
+import com.android.server.power.stats.processor.MultiStatePowerAttributor;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test")
+@Ignore("Performance experiment. Comment out @Ignore to run")
+public class BatteryUsageStatsProviderPerfTest {
+    @Rule
+    public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    private final Clock mClock = new MockClock();
+    private MonotonicClock mMonotonicClock;
+    private PowerProfile mPowerProfile;
+    private CpuScalingPolicies mCpuScalingPolicies;
+    private File mDirectory;
+    private Handler mHandler;
+    private MockBatteryStatsImpl mBatteryStats;
+
+    @Before
+    public void setup() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        mPowerProfile = new PowerProfile(context);
+        mCpuScalingPolicies = new CpuScalingPolicyReader().read();
+
+        HandlerThread mHandlerThread = new HandlerThread("batterystats-handler");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        // Extract accumulated battery history to ensure consistent iterations
+        mDirectory = Files.createTempDirectory("BatteryUsageStatsProviderPerfTest").toFile();
+        File historyDirectory = new File(mDirectory, "battery-history");
+        historyDirectory.mkdir();
+
+        long maxMonotonicTime = 0;
+
+        // To recreate battery-history.zip if necessary, perform these commands:
+        //   cd /tmp
+        //   mkdir battery-history
+        //   adb pull /data/system/battery-history
+        //   zip battery-history.zip battery-history/*
+        //   cp battery-history.zip \
+        //       $ANDROID_BUILD_TOP/frameworks/base/services/tests/powerstatstests/res/raw
+        Resources resources = context.getResources();
+        int resId = resources.getIdentifier("battery-history", "raw", context.getPackageName());
+        try (InputStream in = resources.openRawResource(resId)) {
+            try (ZipInputStream zis = new ZipInputStream(in)) {
+                ZipEntry ze;
+                while ((ze = zis.getNextEntry()) != null) {
+                    if (!ze.getName().endsWith(".bh")) {
+                        continue;
+                    }
+                    File file = new File(mDirectory, ze.getName());
+                    try (OutputStream out = new FileOutputStream(
+                            file)) {
+                        FileUtils.copy(zis, out);
+                    }
+                    long timestamp = Long.parseLong(file.getName().replace(".bh", ""));
+                    if (timestamp > maxMonotonicTime) {
+                        maxMonotonicTime = timestamp;
+                    }
+                }
+            }
+        }
+
+        mMonotonicClock = new MonotonicClock(maxMonotonicTime + 1000000000, mClock);
+        mBatteryStats = new MockBatteryStatsImpl(mClock, mDirectory);
+    }
+
+    @Test
+    public void getBatteryUsageStats_accumulated() {
+        BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+                .setMaxStatsAgeMs(0)
+                .includePowerStateData()
+                .includeScreenStateData()
+                .includeProcessStateData()
+                .accumulated()
+                .build();
+
+        double expectedCpuPower = 0;
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            state.pauseTiming();
+
+            waitForBackgroundThread();
+
+            BatteryUsageStatsProvider provider = createBatteryUsageStatsProvider();
+            state.resumeTiming();
+
+            BatteryUsageStats stats = provider.getBatteryUsageStats(mBatteryStats, query);
+            waitForBackgroundThread();
+
+            state.pauseTiming();
+
+            double cpuConsumedPower = stats.getAggregateBatteryConsumer(
+                            BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                    .getConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU);
+            assertThat(cpuConsumedPower).isNonZero();
+            if (expectedCpuPower == 0) {
+                expectedCpuPower = cpuConsumedPower;
+            } else {
+                // Verify that all iterations produce the same result
+                assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower);
+            }
+            state.resumeTiming();
+        }
+    }
+
+    private BatteryUsageStatsProvider createBatteryUsageStatsProvider() {
+        Context context = InstrumentationRegistry.getContext();
+
+        PowerStatsStore store = new PowerStatsStore(mDirectory, mHandler);
+        store.reset();
+
+        MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(context, store,
+                mPowerProfile, mCpuScalingPolicies, mPowerProfile::getBatteryCapacity);
+        return new BatteryUsageStatsProvider(context, powerAttributor, mPowerProfile,
+                mCpuScalingPolicies, store, 10000000, mClock, mMonotonicClock);
+    }
+
+    private void waitForBackgroundThread() {
+        ConditionVariable done = new ConditionVariable();
+        mHandler.post(done::open);
+        done.block();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
index b5a538f..c7da274 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
@@ -103,7 +103,7 @@
         // NOTE: for now this is only when flag asDeviceConnectionFailure is true
         if (asDeviceConnectionFailure()) {
             when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
-                    AudioSystem.AUDIO_FORMAT_DEFAULT))
+                    AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/))
                     .thenReturn(AudioSystem.AUDIO_STATUS_ERROR);
             runWithBluetoothPrivilegedPermission(
                     () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
@@ -115,7 +115,7 @@
         // test that the device is added when AudioSystem returns AUDIO_STATUS_OK
         // when setDeviceConnectionState is called for the connection
         when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
-                AudioSystem.AUDIO_FORMAT_DEFAULT))
+                AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/))
                 .thenReturn(AudioSystem.AUDIO_STATUS_OK);
         runWithBluetoothPrivilegedPermission(
                 () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index ce59a86..39e7d72 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -51,9 +51,9 @@
     // Overrides of AudioSystemAdapter
     @Override
     public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
-            int codecFormat) {
-        Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s",
-                attributes.toString(), state, Integer.toHexString(codecFormat)));
+            int codecFormat, boolean deviceSwitch) {
+        Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s %b",
+                attributes.toString(), state, Integer.toHexString(codecFormat), deviceSwitch));
         return AudioSystem.AUDIO_STATUS_OK;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 30aa8ce..e0023e5 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1768,6 +1768,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testCertificateDisclosure() throws Exception {
         final int userId = CALLER_USER_HANDLE;
         final UserHandle user = UserHandle.of(userId);
@@ -4612,6 +4613,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testGetLastBugReportRequestTime() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
         setupDeviceOwner();
@@ -4659,6 +4661,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testGetLastNetworkLogRetrievalTime() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
         setupDeviceOwner();
@@ -6441,6 +6444,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception {
         mServiceContext.packageName = mRealTestContext.getPackageName();
         mServiceContext.applicationInfo = new ApplicationInfo();
@@ -6452,6 +6456,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception {
         mServiceContext.packageName = mRealTestContext.getPackageName();
         mServiceContext.applicationInfo = new ApplicationInfo();
@@ -6464,6 +6469,7 @@
     }
 
     @Test
+    @Ignore // b/396073342
     public void testGetOwnerInstalledCaCertsForDelegate() throws Exception {
         mServiceContext.packageName = mRealTestContext.getPackageName();
         mServiceContext.applicationInfo = new ApplicationInfo();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index fca0cfb..cf2c15c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -432,7 +432,7 @@
                         .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
                         .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                         .build()),
-                any(), any(), anyBoolean());
+                anyBoolean(), any(), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
index ec44a91..f44517a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
@@ -112,7 +112,7 @@
                         .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
                         .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                         .build()),
-                any(), any(), anyBoolean());
+                anyBoolean(), any(), any());
     }
 
 
@@ -135,7 +135,7 @@
                         .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
                         .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                         .build()),
-                any(), any(), anyBoolean());
+                anyBoolean(), any(), any());
     }
 
     @Test
@@ -160,7 +160,7 @@
                         .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
                         .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                         .build()),
-                any(), any(), anyBoolean());
+                anyBoolean(), any(), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
index 7294ba6..90f94cb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
@@ -183,9 +183,9 @@
         public void setDeviceAbsoluteVolumeBehavior(
                 @NonNull AudioDeviceAttributes device,
                 @NonNull VolumeInfo volume,
+                boolean handlesVolumeAdjustment,
                 @NonNull @CallbackExecutor Executor executor,
-                @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-                boolean handlesVolumeAdjustment) {
+                @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
             setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
         }
 
@@ -193,9 +193,9 @@
         public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
                 @NonNull AudioDeviceAttributes device,
                 @NonNull VolumeInfo volume,
+                boolean handlesVolumeAdjustment,
                 @NonNull @CallbackExecutor Executor executor,
-                @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-                boolean handlesVolumeAdjustment) {
+                @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
             setVolumeBehaviorHelper(device,
                     AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
         }
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 0eb20eb..66d7611 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -32,6 +32,7 @@
         "androidx.test.rules",
         "hamcrest-library",
         "mockito-target-inline-minus-junit4",
+        "mockito-target-extended",
         "platform-compat-test-rules",
         "platform-test-annotations",
         "platformprotosnano",
diff --git a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
index 779fa1a..dbbe40f 100644
--- a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
+++ b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
@@ -80,7 +80,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualActivation() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -111,7 +111,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualDeactivation() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -145,7 +145,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_respectsManuallyActivated() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -178,7 +178,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_respectsManuallyDeactivated() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -212,7 +212,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualActivationFromApp() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -244,7 +244,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+    @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualDeactivationFromApp() {
         AutomaticZenRule ruleToCreate = createZenRule("rule");
         String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index c4b8599..9930c9f 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -70,7 +70,6 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
-import android.app.Flags;
 import android.app.IOnProjectionStateChangedListener;
 import android.app.IUiModeManager;
 import android.content.BroadcastReceiver;
@@ -91,7 +90,6 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.test.FakePermissionEnforcer;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.service.dreams.DreamManagerInternal;
@@ -1508,13 +1506,11 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void testAttentionModeThemeOverlay_nightModeDisabled() throws RemoteException {
         testAttentionModeThemeOverlay(false);
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void testAttentionModeThemeOverlay_nightModeEnabled() throws RemoteException {
         testAttentionModeThemeOverlay(true);
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
index b3ec215..c9d5241 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
@@ -30,6 +30,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
 
 import org.junit.After;
@@ -41,6 +42,7 @@
 
 public class UiServiceTestCase {
     @Mock protected PackageManagerInternal mPmi;
+    @Mock protected UserManagerInternal mUmi;
     @Mock protected UriGrantsManagerInternal mUgmInternal;
 
     protected static final String PKG_N_MR1 = "com.example.n_mr1";
@@ -92,6 +94,8 @@
                     }
                 });
 
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+        LocalServices.addService(UserManagerInternal.class, mUmi);
         LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
         LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
         when(mUgmInternal.checkGrantUriPermission(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 1890879..5ce9a3e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -47,7 +47,6 @@
 import android.content.IntentFilter;
 import android.hardware.display.ColorDisplayManager;
 import android.os.PowerManager;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenDeviceEffects;
 import android.testing.TestableContext;
@@ -102,8 +101,6 @@
 
     @Test
     public void apply_appliesEffects() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
                 .setShouldSuppressAmbientDisplay(true)
                 .setShouldDimWallpaper(true)
@@ -119,7 +116,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void apply_logsToZenLog() {
         when(mPowerManager.isInteractive()).thenReturn(true);
         ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
@@ -155,8 +151,6 @@
 
     @Test
     public void apply_removesEffects() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder()
                 .setShouldSuppressAmbientDisplay(true)
                 .setShouldDimWallpaper(true)
@@ -180,8 +174,6 @@
 
     @Test
     public void apply_removesOnlyPreviouslyAppliedEffects() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder()
                 .setShouldSuppressAmbientDisplay(true)
                 .build();
@@ -197,7 +189,6 @@
 
     @Test
     public void apply_missingSomeServices_okay() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mContext.addMockSystemService(ColorDisplayManager.class, null);
         mContext.addMockSystemService(WallpaperManager.class, null);
         mApplier = new DefaultDeviceEffectsApplier(mContext);
@@ -216,7 +207,6 @@
 
     @Test
     public void apply_disabledWallpaperService_dimWallpaperNotApplied() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         WallpaperManager disabledWallpaperService = mock(WallpaperManager.class);
         when(mWallpaperManager.isWallpaperSupported()).thenReturn(false);
         mContext.addMockSystemService(WallpaperManager.class, disabledWallpaperService);
@@ -236,8 +226,6 @@
 
     @Test
     public void apply_someEffects_onlyThoseEffectsApplied() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
                 .setShouldDimWallpaper(true)
                 .setShouldDisplayGrayscale(true)
@@ -253,8 +241,6 @@
 
     @Test
     public void apply_onlyEffectDeltaApplied() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         mApplier.apply(new ZenDeviceEffects.Builder().setShouldDimWallpaper(true).build(),
                 ORIGIN_USER_IN_SYSTEMUI);
         verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f));
@@ -272,7 +258,6 @@
 
     @Test
     public void apply_nightModeFromApp_appliedOnScreenOff() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         ArgumentCaptor<IntentFilter> intentFilterCaptor =
@@ -301,8 +286,6 @@
     @Test
     public void apply_nightModeWithScreenOff_appliedImmediately(
             @TestParameter ZenChangeOrigin origin) {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         when(mPowerManager.isInteractive()).thenReturn(false);
 
         mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -314,7 +297,6 @@
     }
 
     @Test
-    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
     public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately(
             @TestParameter ZenChangeOrigin origin) {
 
@@ -334,8 +316,6 @@
             "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"})
     public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(
             ZenChangeOrigin origin) {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         when(mPowerManager.isInteractive()).thenReturn(true);
 
         mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -351,8 +331,6 @@
             "{origin: ORIGIN_SYSTEM}", "{origin: ORIGIN_UNKNOWN}"})
     public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(
             ZenChangeOrigin origin) {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         when(mPowerManager.isInteractive()).thenReturn(true);
 
         mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -367,8 +345,6 @@
 
     @Test
     public void apply_servicesThrow_noCrash() {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
         doThrow(new RuntimeException()).when(mPowerManager)
                 .suppressAmbientDisplay(anyString(), anyBoolean());
         doThrow(new RuntimeException()).when(mColorDisplayManager).setSaturationLevel(anyInt());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index e5c42082..98440ec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -17,12 +17,17 @@
 
 import static android.content.Context.DEVICE_POLICY_SERVICE;
 import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
+import static android.os.UserHandle.USER_ALL;
+import static android.os.UserHandle.USER_CURRENT;
 import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
 import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
 import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
 import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT;
 import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE;
 import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
@@ -66,7 +71,9 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -83,6 +90,7 @@
 import com.google.android.collect.Lists;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -105,6 +113,9 @@
 
 public class ManagedServicesTest extends UiServiceTestCase {
 
+    @Rule
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock
     private IPackageManager mIpm;
     @Mock
@@ -155,6 +166,7 @@
         users.add(new UserInfo(11, "11", 0));
         users.add(new UserInfo(12, "12", 0));
         users.add(new UserInfo(13, "13", 0));
+        users.add(new UserInfo(99, "99", 0));
         for (UserInfo user : users) {
             when(mUm.getUserInfo(eq(user.id))).thenReturn(user);
         }
@@ -804,6 +816,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void rebindServices_onlyBindsExactMatchesIfComponent() throws Exception {
         // If the primary and secondary lists contain component names, only those components within
         // the package should be matched
@@ -841,6 +854,45 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void rebindServices_onlyBindsExactMatchesIfComponent_concurrent_multiUser()
+            throws Exception {
+        // If the primary and secondary lists contain component names, only those components within
+        // the package should be matched
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
+                mIpm,
+                ManagedServices.APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        packages.add("anotherPackage");
+        addExpectedServices(service, packages, 0);
+
+        // only 2 components are approved per package
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryComponentNames.put(0, "anotherPackage/C1:anotherPackage/C2");
+
+        loadXml(service);
+        // verify the 2 components per package are enabled (bound)
+        verifyExpectedBoundEntries(service, true, 0);
+        verifyExpectedBoundEntries(service, false, 0);
+
+        // verify the last component per package is not enabled/we don't try to bind to it
+        for (String pkg : packages) {
+            ComponentName unapprovedAdditionalComponent =
+                    ComponentName.unflattenFromString(pkg + "/C3");
+            assertFalse(
+                    service.isComponentEnabledForUser(
+                            unapprovedAdditionalComponent, 0));
+            verify(mIpm, never()).getServiceInfo(
+                    eq(unapprovedAdditionalComponent), anyLong(), anyInt());
+        }
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void rebindServices_bindsEverythingInAPackage() throws Exception {
         // If the primary and secondary lists contain packages, all components within those packages
         // should be bound
@@ -866,6 +918,32 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void rebindServices_bindsEverythingInAPackage_concurrent_multiUser() throws Exception {
+        // If the primary and secondary lists contain packages, all components within those packages
+        // should be bound
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_PACKAGE);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        packages.add("packagea");
+        addExpectedServices(service, packages, 0);
+
+        // 2 approved packages
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryPackages.put(0, "package");
+        mExpectedSecondaryPackages.clear();
+        mExpectedSecondaryPackages.put(0, "packagea");
+
+        loadXml(service);
+
+        // verify the 3 components per package are enabled (bound)
+        verifyExpectedBoundEntries(service, true, 0);
+        verifyExpectedBoundEntries(service, false, 0);
+    }
+
+    @Test
     public void reregisterService_checksAppIsApproved_pkg() throws Exception {
         Context context = mock(Context.class);
         PackageManager pm = mock(PackageManager.class);
@@ -1118,6 +1196,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void testUpgradeAppBindsNewServices() throws Exception {
         // If the primary and secondary lists contain component names, only those components within
         // the package should be matched
@@ -1159,6 +1238,49 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUpgradeAppBindsNewServices_concurrent_multiUser() throws Exception {
+        // If the primary and secondary lists contain component names, only those components within
+        // the package should be matched
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
+                mIpm,
+                ManagedServices.APPROVAL_BY_PACKAGE);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        // only 2 components are approved per package
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        // new component expected
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2:package/C3");
+
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        // verify the 3 components per package are enabled (bound)
+        verifyExpectedBoundEntries(service, true, 0);
+
+        // verify the last component per package is not enabled/we don't try to bind to it
+        for (String pkg : packages) {
+            ComponentName unapprovedAdditionalComponent =
+                    ComponentName.unflattenFromString(pkg + "/C3");
+            assertFalse(
+                    service.isComponentEnabledForUser(
+                            unapprovedAdditionalComponent, 0));
+            verify(mIpm, never()).getServiceInfo(
+                    eq(unapprovedAdditionalComponent), anyLong(), anyInt());
+        }
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void testUpgradeAppNoPermissionNoRebind() throws Exception {
         Context context = spy(getContext());
         doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
@@ -1211,6 +1333,59 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUpgradeAppNoPermissionNoRebind_concurrent_multiUser() throws Exception {
+        Context context = spy(getContext());
+        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+                mIpm,
+                APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+        // Both components are approved initially
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        //Component package/C1 loses bind permission
+        when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+                (Answer<ServiceInfo>) invocation -> {
+                    ComponentName invocationCn = invocation.getArgument(0);
+                    if (invocationCn != null) {
+                        ServiceInfo serviceInfo = new ServiceInfo();
+                        serviceInfo.packageName = invocationCn.getPackageName();
+                        serviceInfo.name = invocationCn.getClassName();
+                        if (invocationCn.equals(unapprovedComponent)) {
+                            serviceInfo.permission = "none";
+                        } else {
+                            serviceInfo.permission = service.getConfig().bindPermission;
+                        }
+                        serviceInfo.metaData = null;
+                        return serviceInfo;
+                    }
+                    return null;
+                }
+        );
+
+        // Trigger package update
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        assertFalse(service.isComponentEnabledForUser(unapprovedComponent, 0));
+        assertTrue(service.isComponentEnabledForUser(approvedComponent, 0));
+    }
+
+    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1517,6 +1692,201 @@
         assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c")));
     }
 
+    @SuppressWarnings("GuardedBy")
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testPopulateComponentsToBindWithNonProfileUser() {
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        spyOn(service);
+
+        SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>();
+        ArraySet<ComponentName> allowed0 = new ArraySet<>();
+        allowed0.add(ComponentName.unflattenFromString("a/a"));
+        approvedComponentsByUser.put(0, allowed0);
+        ArraySet<ComponentName> allowed10 = new ArraySet<>();
+        allowed10.add(ComponentName.unflattenFromString("b/b"));
+        approvedComponentsByUser.put(10, allowed10);
+
+        int nonProfileUser = 99;
+        ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>();
+        allowedForNonProfileUser.add(ComponentName.unflattenFromString("c/c"));
+        approvedComponentsByUser.put(nonProfileUser, allowedForNonProfileUser);
+
+        IntArray users = new IntArray();
+        users.add(nonProfileUser);
+        users.add(10);
+        users.add(0);
+
+        SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
+        spyOn(service.mUmInternal);
+        when(service.mUmInternal.isVisibleBackgroundFullUser(nonProfileUser)).thenReturn(true);
+
+        service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser);
+
+        assertTrue(service.isComponentEnabledForUser(
+                ComponentName.unflattenFromString("a/a"), 0));
+        assertTrue(service.isComponentEnabledForPackage("a", 0));
+        assertTrue(service.isComponentEnabledForUser(
+                ComponentName.unflattenFromString("b/b"), 10));
+        assertTrue(service.isComponentEnabledForPackage("b", 0));
+        assertTrue(service.isComponentEnabledForPackage("b", 10));
+        assertTrue(service.isComponentEnabledForUser(
+                ComponentName.unflattenFromString("c/c"), nonProfileUser));
+        assertTrue(service.isComponentEnabledForPackage("c", nonProfileUser));
+    }
+
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testRebindService_profileUser() throws Exception {
+        final int profileUserId = 10;
+        when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+        spyOn(mService);
+        ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+                IntArray.class);
+        when(mService.allowRebindForParentUser()).thenReturn(true);
+
+        mService.rebindServices(false, profileUserId);
+
+        verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+        assertTrue(captor.getValue().contains(0));
+        assertTrue(captor.getValue().contains(profileUserId));
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testRebindService_nonProfileUser() throws Exception {
+        final int userId = 99;
+        when(mUserProfiles.isProfileUser(userId, mContext)).thenReturn(false);
+        spyOn(mService);
+        ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+                IntArray.class);
+        when(mService.allowRebindForParentUser()).thenReturn(true);
+
+        mService.rebindServices(false, userId);
+
+        verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+        assertFalse(captor.getValue().contains(0));
+        assertTrue(captor.getValue().contains(userId));
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testRebindService_userAll() throws Exception {
+        final int userId = 99;
+        spyOn(mService);
+        spyOn(mService.mUmInternal);
+        when(mService.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+        ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+                IntArray.class);
+        when(mService.allowRebindForParentUser()).thenReturn(true);
+
+        mService.rebindServices(false, USER_ALL);
+
+        verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+        assertTrue(captor.getValue().contains(0));
+        assertTrue(captor.getValue().contains(userId));
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testOnUserStoppedWithVisibleBackgroundUser() throws Exception {
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        spyOn(service);
+        int userId = 99;
+        SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>();
+        ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>();
+        allowedForNonProfileUser.add(ComponentName.unflattenFromString("a/a"));
+        approvedComponentsByUser.put(userId, allowedForNonProfileUser);
+        IntArray users = new IntArray();
+        users.add(userId);
+        SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
+        spyOn(service.mUmInternal);
+        when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+        service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser);
+        assertTrue(service.isComponentEnabledForUser(
+                ComponentName.unflattenFromString("a/a"), userId));
+        assertTrue(service.isComponentEnabledForPackage("a", userId));
+
+        service.onUserStopped(userId);
+
+        assertFalse(service.isComponentEnabledForUser(
+                ComponentName.unflattenFromString("a/a"), userId));
+        assertFalse(service.isComponentEnabledForPackage("a", userId));
+        verify(service).unbindUserServices(eq(userId));
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUnbindServicesImpl_serviceOfForegroundUser() throws Exception {
+        int switchingUserId = 10;
+        int userId = 99;
+
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        spyOn(service);
+        spyOn(service.mUmInternal);
+        when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(false);
+
+        IInterface iInterface = mock(IInterface.class);
+        when(iInterface.asBinder()).thenReturn(mock(IBinder.class));
+
+        ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo(
+                iInterface, ComponentName.unflattenFromString("a/a"), userId, false,
+                mock(ServiceConnection.class), 26, 34);
+
+        Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>();
+        removableBoundServices.add(serviceInfo);
+
+        when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices);
+        ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass(
+                SparseArray.class);
+
+        service.unbindServicesImpl(switchingUserId, true);
+
+        verify(service).unbindFromServices(captor.capture());
+
+        assertEquals(captor.getValue().size(), 1);
+        assertTrue(captor.getValue().indexOfKey(userId) != -1);
+        assertTrue(captor.getValue().get(userId).contains(
+                ComponentName.unflattenFromString("a/a")));
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUnbindServicesImpl_serviceOfVisibleBackgroundUser() throws Exception {
+        int switchingUserId = 10;
+        int userId = 99;
+
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        spyOn(service);
+        spyOn(service.mUmInternal);
+        when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+
+        IInterface iInterface = mock(IInterface.class);
+        when(iInterface.asBinder()).thenReturn(mock(IBinder.class));
+
+        ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo(
+                iInterface, ComponentName.unflattenFromString("a/a"), userId,
+                false, mock(ServiceConnection.class), 26, 34);
+
+        Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>();
+        removableBoundServices.add(serviceInfo);
+
+        when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices);
+        ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass(
+                SparseArray.class);
+
+        service.unbindServicesImpl(switchingUserId, true);
+
+        verify(service).unbindFromServices(captor.capture());
+
+        assertEquals(captor.getValue().size(), 0);
+    }
+
     @Test
     public void testOnNullBinding() throws Exception {
         Context context = mock(Context.class);
@@ -1681,6 +2051,7 @@
         assertFalse(service.isBound(cn, mZero.id));
         assertFalse(service.isBound(cn, mTen.id));
     }
+
     @Test
     public void testOnPackagesChanged_nullValuesPassed_noNullPointers() {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
@@ -2012,6 +2383,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isComponentEnabledForCurrentProfiles_isThreadSafe() throws InterruptedException {
         for (UserInfo userInfo : mUm.getUsers()) {
             mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true);
@@ -2024,6 +2396,20 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isComponentEnabledForUser_isThreadSafe() throws InterruptedException {
+        for (UserInfo userInfo : mUm.getUsers()) {
+            mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true);
+        }
+        testThreadSafety(() -> {
+            mService.rebindServices(false, 0);
+            assertThat(mService.isComponentEnabledForUser(
+                    new ComponentName("pkg1", "cmp1"), 0)).isTrue();
+        }, 20, 30);
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isComponentEnabledForCurrentProfiles_profileUserId() {
         final int profileUserId = 10;
         when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
@@ -2037,6 +2423,24 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isComponentEnabledForUser_profileUserId() {
+        final int profileUserId = 10;
+        when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+        spyOn(mService);
+        doReturn(USER_CURRENT).when(mService).resolveUserId(anyInt());
+
+        // Only approve for parent user (0)
+        mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true);
+
+        // Test that the component is enabled after calling rebindServices with profile userId (10)
+        mService.rebindServices(false, profileUserId);
+        assertThat(mService.isComponentEnabledForUser(
+                new ComponentName("pkg1", "cmp1"), profileUserId)).isTrue();
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() {
         final int profileUserId = 10;
         when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
@@ -2054,6 +2458,25 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isComponentEnabledForUser_profileUserId_NAS() {
+        final int profileUserId = 10;
+        when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+        // Do not rebind for parent users (NAS use-case)
+        ManagedServices service = spy(mService);
+        when(service.allowRebindForParentUser()).thenReturn(false);
+        doReturn(USER_CURRENT).when(service).resolveUserId(anyInt());
+
+        // Only approve for parent user (0)
+        service.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true);
+
+        // Test that the component is disabled after calling rebindServices with profile userId (10)
+        service.rebindServices(false, profileUserId);
+        assertThat(service.isComponentEnabledForUser(
+                new ComponentName("pkg1", "cmp1"), profileUserId)).isFalse();
+    }
+
+    @Test
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     public void testManagedServiceInfoIsSystemUi() {
         ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
@@ -2069,6 +2492,48 @@
         assertThat(service0.isSystemUi()).isFalse();
     }
 
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUserMatchesAndEnabled_profileUser() throws Exception {
+        int currentUserId = 10;
+        int profileUserId = 11;
+
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo(
+                mock(IInterface.class), ComponentName.unflattenFromString("a/a"), currentUserId,
+                false, mock(ServiceConnection.class), 26, 34));
+
+        doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId);
+        doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId);
+        doReturn(true).when(listener).isEnabledForUser();
+        doReturn(true).when(mUserProfiles).isCurrentProfile(anyInt());
+
+        assertThat(listener.enabledAndUserMatches(profileUserId)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void testUserMatchesAndDisabled_visibleBackgroudUser() throws Exception {
+        int currentUserId = 10;
+        int profileUserId = 11;
+        int visibleBackgroundUserId = 12;
+
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+        ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo(
+                mock(IInterface.class), ComponentName.unflattenFromString("a/a"), profileUserId,
+                false, mock(ServiceConnection.class), 26, 34));
+
+        doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId);
+        doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId);
+        doReturn(visibleBackgroundUserId).when(service.mUmInternal)
+                .getProfileParentId(visibleBackgroundUserId);
+        doReturn(true).when(listener).isEnabledForUser();
+
+        assertThat(listener.enabledAndUserMatches(visibleBackgroundUserId)).isFalse();
+    }
+
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
             ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
             throws RemoteException {
@@ -2247,26 +2712,47 @@
 
     private void verifyExpectedBoundEntries(ManagedServices service, boolean primary)
             throws Exception {
+        verifyExpectedBoundEntries(service, primary, UserHandle.USER_CURRENT);
+    }
+
+    private void verifyExpectedBoundEntries(ManagedServices service, boolean primary,
+            int targetUserId) throws Exception {
         ArrayMap<Integer, String> verifyMap = primary ? mExpectedPrimary.get(service.mApprovalLevel)
                 : mExpectedSecondary.get(service.mApprovalLevel);
         for (int userId : verifyMap.keySet()) {
             for (String packageOrComponent : verifyMap.get(userId).split(":")) {
                 if (!TextUtils.isEmpty(packageOrComponent)) {
                     if (service.mApprovalLevel == APPROVAL_BY_PACKAGE) {
-                        assertTrue(packageOrComponent,
-                                service.isComponentEnabledForPackage(packageOrComponent));
+                        if (managedServicesConcurrentMultiuser()) {
+                            assertTrue(packageOrComponent,
+                                    service.isComponentEnabledForPackage(packageOrComponent,
+                                            targetUserId));
+                        } else {
+                            assertTrue(packageOrComponent,
+                                    service.isComponentEnabledForPackage(packageOrComponent));
+                        }
                         for (int i = 1; i <= 3; i++) {
                             ComponentName componentName = ComponentName.unflattenFromString(
                                     packageOrComponent +"/C" + i);
-                            assertTrue(service.isComponentEnabledForCurrentProfiles(
-                                    componentName));
+                            if (managedServicesConcurrentMultiuser()) {
+                                assertTrue(service.isComponentEnabledForUser(
+                                        componentName, targetUserId));
+                            } else {
+                                assertTrue(service.isComponentEnabledForCurrentProfiles(
+                                        componentName));
+                            }
                             verify(mIpm, times(1)).getServiceInfo(
                                     eq(componentName), anyLong(), anyInt());
                         }
                     } else {
                         ComponentName componentName =
                                 ComponentName.unflattenFromString(packageOrComponent);
-                        assertTrue(service.isComponentEnabledForCurrentProfiles(componentName));
+                        if (managedServicesConcurrentMultiuser()) {
+                            assertTrue(service.isComponentEnabledForUser(componentName,
+                                    targetUserId));
+                        } else {
+                            assertTrue(service.isComponentEnabledForCurrentProfiles(componentName));
+                        }
                         verify(mIpm, times(1)).getServiceInfo(
                                 eq(componentName), anyLong(), anyInt());
                     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 415e3ac..37ab541 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -140,6 +140,7 @@
 import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
 import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
 import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
 import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS;
 import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY;
 import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION;
@@ -867,7 +868,8 @@
                     && filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
                 mPackageIntentReceiver = broadcastReceivers.get(i);
             }
-            if (filter.hasAction(Intent.ACTION_USER_SWITCHED)
+            if (filter.hasAction(Intent.ACTION_USER_STOPPED)
+                    || filter.hasAction(Intent.ACTION_USER_SWITCHED)
                     || filter.hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)
                     || filter.hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) {
                 // There may be multiple receivers, get the NMS one
@@ -11028,7 +11030,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11046,20 +11047,17 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeManagedCanBeUsedBySystem() throws Exception {
         addAutomaticZenRule_restrictedRuleTypeCanBeUsedBySystem(AutomaticZenRule.TYPE_MANAGED);
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeManagedCannotBeUsedByRegularApps() throws Exception {
         addAutomaticZenRule_restrictedRuleTypeCannotBeUsedByRegularApps(
                 AutomaticZenRule.TYPE_MANAGED);
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeBedtimeCanBeUsedByWellbeing() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11082,7 +11080,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeBedtimeCanBeUsedBySystem() throws Exception {
         reset(mPackageManagerInternal);
         when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true);
@@ -11090,7 +11087,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeBedtimeCannotBeUsedByRegularApps() throws Exception {
         reset(mPackageManagerInternal);
         when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true);
@@ -11133,7 +11129,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.isSystemUid = true;
@@ -11145,7 +11140,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.isSystemUid = true;
@@ -11157,7 +11151,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11169,7 +11162,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception {
         setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11179,7 +11171,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.isSystemUid = true;
@@ -11191,7 +11182,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
         setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11201,7 +11191,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.isSystemUid = true;
@@ -11213,7 +11202,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
         setUpMockZenTest();
         mService.setCallerIsNormalPackage();
@@ -11223,7 +11211,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setAutomaticZenRuleState_fromAppWithConditionFromUser_originUserInApp()
             throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11238,7 +11225,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_originApp()
             throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11253,7 +11239,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setAutomaticZenRuleState_fromSystemWithConditionFromUser_originUserInSystemUi()
             throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11267,7 +11252,6 @@
                 eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyInt());
     }
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_originSystem()
             throws Exception {
         ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11438,7 +11422,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void onAutomaticRuleStatusChanged_sendsBroadcastToRuleOwner() throws Exception {
         mService.mZenModeHelper.getCallbacks().forEach(c -> c.onAutomaticRuleStatusChanged(
                 mUserId, "rule.owner.pkg", "rule_id", AUTOMATIC_RULE_STATUS_ACTIVATED));
@@ -16302,7 +16285,7 @@
 
         InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
         inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
-        inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+        inOrder.verify(mPreferencesHelper).syncHasPriorityChannels();
         inOrder.verifyNoMoreInteractions();
     }
 
@@ -16318,11 +16301,25 @@
 
         InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
         inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
-        inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+        inOrder.verify(mPreferencesHelper).syncHasPriorityChannels();
         inOrder.verifyNoMoreInteractions();
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void onUserStopped_callBackToListeners() {
+        Intent intent = new Intent(Intent.ACTION_USER_STOPPED);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+
+        mUserIntentReceiver.onReceive(mContext, intent);
+
+        verify(mConditionProviders).onUserStopped(eq(20));
+        verify(mListeners).onUserStopped(eq(20));
+        verify(mAssistants).onUserStopped(eq(20));
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception {
         final String notReal = "NOT REAL";
         final var checker = mService.permissionChecker;
@@ -16339,6 +16336,25 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_invalidPackage_concurrent_multiUser()
+                throws Exception {
+        final String notReal = "NOT REAL";
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow(
+                PackageManager.NameNotFoundException.class);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt());
+        verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean());
+        verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt());
+        verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+        verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_hasPermission() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16357,6 +16373,27 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_hasPermission_concurrent_multiUser()
+                throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+        when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+        verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16375,6 +16412,27 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_isPackageAllowed_concurrent_multiUser()
+                throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+        when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt()))
+                .thenReturn(true);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+        verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16392,6 +16450,26 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_isComponentEnabled_concurrent_multiUser()
+                throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+        when(mListeners.isComponentEnabledForPackage(packageName, mUserId)).thenReturn(true);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+        verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16408,10 +16486,30 @@
         verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
     }
 
+    @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_isDeviceOwner_concurrent_multiUser()
+            throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+        when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+        verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+    }
+
     /**
      * b/292163859
      */
     @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16430,7 +16528,32 @@
         verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
     }
 
+    /**
+     * b/292163859
+     */
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_callerIsDeviceOwner_concurrent_multiUser()
+                throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final int callingUid = Binder.getCallingUid();
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+        when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+        verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+        verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
+    }
+
+    @Test
+    @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
     public void isNotificationPolicyAccessGranted_notGranted() throws Exception {
         final String packageName = "target";
         final int uid = 123;
@@ -16447,6 +16570,24 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+    public void isNotificationPolicyAccessGranted_notGranted_concurrent_multiUser()
+                throws Exception {
+        final String packageName = "target";
+        final int uid = 123;
+        final var checker = mService.permissionChecker;
+
+        when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+
+        assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+        verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+        verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+        verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+        verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+        verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+    }
+
+    @Test
     public void testResetDefaultDnd() {
         TestableNotificationManagerService service = spy(mService);
         UserInfo user = new UserInfo(0, "owner", 0);
@@ -16481,7 +16622,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setDeviceEffectsApplier_succeeds() throws Exception {
         initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
@@ -16492,7 +16632,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setDeviceEffectsApplier_tooLate_throws() throws Exception {
         initNMS(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
 
@@ -16501,7 +16640,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void setDeviceEffectsApplier_calledTwice_throws() throws Exception {
         initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
@@ -16513,7 +16651,6 @@
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenHelper;
@@ -16530,7 +16667,6 @@
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
@@ -16570,7 +16706,6 @@
     private void setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
             @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
             throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
@@ -16597,7 +16732,6 @@
     @Test
     @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
@@ -16613,7 +16747,6 @@
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenHelper;
@@ -16628,7 +16761,6 @@
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenHelper;
@@ -16644,7 +16776,6 @@
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
@@ -16683,7 +16814,6 @@
     private void setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
             @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
             throws RemoteException {
-        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
         mService.setCallerIsNormalPackage();
@@ -16708,7 +16838,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void requestInterruptionFilterFromListener_fromApp_doesNotSetGlobalZen()
             throws Exception {
@@ -16726,7 +16855,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void requestInterruptionFilterFromListener_fromSystem_setsGlobalZen()
             throws Exception {
@@ -16745,24 +16873,6 @@
     }
 
     @Test
-    @DisableFlags(android.app.Flags.FLAG_MODES_API)
-    public void requestInterruptionFilterFromListener_flagOff_callsRequestFromListener()
-            throws Exception {
-        mService.setCallerIsNormalPackage();
-        mService.mZenModeHelper = mock(ZenModeHelper.class);
-        ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
-        when(mListeners.checkServiceTokenLocked(any())).thenReturn(info);
-        info.component = new ComponentName("pkg", "cls");
-
-        mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class),
-                INTERRUPTION_FILTER_PRIORITY);
-
-        verify(mService.mZenModeHelper).requestFromListener(eq(info.component),
-                eq(INTERRUPTION_FILTER_PRIORITY), eq(mUid), /* fromSystemOrSystemUi= */ eq(false));
-    }
-
-    @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void updateAutomaticZenRule_implicitRuleWithoutCPS_disallowedFromApp() throws Exception {
         setUpRealZenTest();
@@ -16788,7 +16898,6 @@
     }
 
     @Test
-    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void updateAutomaticZenRule_implicitRuleWithoutCPS_allowedFromSystem() throws Exception {
         setUpRealZenTest();
@@ -16814,7 +16923,7 @@
     }
 
     @Test
-    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    @EnableFlags(android.app.Flags.FLAG_MODES_UI)
     public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed()
             throws Exception {
         setUpRealZenTest();
@@ -16844,7 +16953,7 @@
     }
 
     @Test
-    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    @EnableFlags(android.app.Flags.FLAG_MODES_UI)
     @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
     public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState()
             throws Exception {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 640de17..5dea44d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -363,7 +363,7 @@
                 .when(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI));
 
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
         when(mAppOpsManager.noteOpNoThrow(anyInt(), anyInt(),
                 anyString(), eq(null), anyString())).thenReturn(MODE_DEFAULT);
@@ -2733,7 +2733,7 @@
         NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2748,7 +2748,7 @@
         channel2.setBypassDnd(true);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
                 uid, false);
-        assertTrue(mHelper.areChannelsBypassingDnd());
+        assertTrue(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(true));
@@ -2760,7 +2760,7 @@
 
         // delete channels
         mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
-        assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
+        assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2770,7 +2770,7 @@
         resetZenModeHelper();
 
         mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -2792,7 +2792,7 @@
         NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2807,7 +2807,7 @@
         mHelper.createNotificationChannel(PKG_N_MR1, uid, update, true, true,
                 uid, false);
 
-        assertTrue(mHelper.areChannelsBypassingDnd());
+        assertTrue(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(true));
@@ -2829,7 +2829,7 @@
         NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2844,7 +2844,7 @@
         channel2.setBypassDnd(true);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
                 uid, false);
-        assertTrue(mHelper.areChannelsBypassingDnd());
+        assertTrue(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(true));
@@ -2856,7 +2856,7 @@
 
         // delete channels
         mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
-        assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
+        assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2866,7 +2866,7 @@
         resetZenModeHelper();
 
         mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -2884,9 +2884,9 @@
 
         // start in a 'allowed to bypass dnd state'
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
-        mHelper.syncChannelsBypassingDnd();
+        mHelper.syncHasPriorityChannels();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2899,7 +2899,7 @@
         channel2.setBypassDnd(true);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -2917,9 +2917,9 @@
 
         // start in a 'allowed to bypass dnd state'
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
-        mHelper.syncChannelsBypassingDnd();
+        mHelper.syncHasPriorityChannels();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2927,7 +2927,7 @@
         channel2.setBypassDnd(true);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -2945,9 +2945,9 @@
 
         // start in a 'allowed to bypass dnd state'
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
-        mHelper.syncChannelsBypassingDnd();
+        mHelper.syncHasPriorityChannels();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2955,7 +2955,7 @@
         channel2.setBypassDnd(true);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -2977,7 +2977,7 @@
         NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
         mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
                 uid, false);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -2990,7 +2990,7 @@
         // expected result: areChannelsBypassingDnd = true
         channel.setBypassDnd(true);
         mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
-        assertTrue(mHelper.areChannelsBypassingDnd());
+        assertTrue(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(true));
@@ -3004,7 +3004,7 @@
         // expected result: areChannelsBypassingDnd = false
         channel.setBypassDnd(false);
         mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -3020,10 +3020,10 @@
         // start notification policy off with mAreChannelsBypassingDnd = true, but
         // RankingHelper should change to false
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
-        mHelper.syncChannelsBypassingDnd();
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        mHelper.syncHasPriorityChannels();
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
                     eq(false));
@@ -3039,7 +3039,7 @@
         // start notification policy off with mAreChannelsBypassingDnd = false
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
-        assertFalse(mHelper.areChannelsBypassingDnd());
+        assertFalse(mHelper.hasPriorityChannels());
         if (android.app.Flags.modesUi()) {
             verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
         } else {
@@ -3050,7 +3050,7 @@
     }
 
     @Test
-    public void syncChannelsBypassingDnd_includesProfilesOfCurrentUser() throws Exception {
+    public void syncHasPriorityChannels_includesProfilesOfCurrentUser() throws Exception {
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0, 10}));
         when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true);
         ApplicationInfo appInfo = new ApplicationInfo();
@@ -3067,13 +3067,13 @@
         mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass,
                 false, false, Process.SYSTEM_UID, true);
 
-        mHelper.syncChannelsBypassingDnd();
+        mHelper.syncHasPriorityChannels();
 
-        assertThat(mHelper.areChannelsBypassingDnd()).isTrue();
+        assertThat(mHelper.hasPriorityChannels()).isTrue();
     }
 
     @Test
-    public void syncChannelsBypassingDnd_excludesOtherUsers() throws Exception {
+    public void syncHasPriorityChannels_excludesOtherUsers() throws Exception {
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0}));
         when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true);
         ApplicationInfo appInfo = new ApplicationInfo();
@@ -3090,9 +3090,9 @@
         mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass,
                 false, false, Process.SYSTEM_UID, true);
 
-        mHelper.syncChannelsBypassingDnd();
+        mHelper.syncHasPriorityChannels();
 
-        assertThat(mHelper.areChannelsBypassingDnd()).isFalse();
+        assertThat(mHelper.hasPriorityChannels()).isFalse();
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index f900346..ec428d5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -17,9 +17,10 @@
 
 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
-
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertEquals;
 
 import static org.junit.Assert.assertTrue;
@@ -29,7 +30,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -40,7 +40,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
-import android.media.AudioAttributes;
 import android.net.Uri;
 import android.os.Build;
 import android.os.UserHandle;
@@ -67,7 +66,6 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -154,7 +152,7 @@
                 .thenReturn(SOUND_URI);
 
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+                NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
         when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
         mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
                 mUsageStats, new String[] {ImportanceExtractor.class.getName()},
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 75552bc..f381343 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -20,10 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.Flags;
 import android.app.NotificationManager.Policy;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
 
@@ -137,8 +134,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
-    public void notificationPolicyToZenPolicy_modesApi_priorityChannels() {
+    public void notificationPolicyToZenPolicy_priorityChannels() {
         Policy policy = new Policy(0, 0, 0, 0,
                 Policy.policyState(false, true), 0);
 
@@ -151,20 +147,4 @@
         assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
                 ZenPolicy.STATE_DISALLOW);
     }
-
-    @Test
-    @DisableFlags(Flags.FLAG_MODES_API)
-    public void notificationPolicyToZenPolicy_noModesApi_priorityChannelsUnset() {
-        Policy policy = new Policy(0, 0, 0, 0,
-                Policy.policyState(false, true), 0);
-
-        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
-        assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
-
-        Policy notAllowed = new Policy(0, 0, 0, 0,
-                Policy.policyState(false, false), 0);
-        ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
-        assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
-                ZenPolicy.STATE_UNSET);
-    }
 }
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 af911e8..9a2b748 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -20,7 +20,6 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.app.Flags;
 import android.os.Parcel;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenDeviceEffects;
@@ -31,7 +30,6 @@
 
 import com.google.common.collect.ImmutableSet;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,11 +40,6 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
-    @Before
-    public final void setUp() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-    }
-
     @Test
     public void builder() {
         ZenDeviceEffects deviceEffects =
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 b42a6a5..67efb9e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -174,7 +174,7 @@
         }
         assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
 
-        config.areChannelsBypassingDnd = true;
+        config.hasPriorityChannels = true;
         assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
 
         if (Flags.modesUi()) {
@@ -187,7 +187,7 @@
 
         assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
 
-        config.areChannelsBypassingDnd = false;
+        config.hasPriorityChannels = false;
         if (Flags.modesUi()) {
             config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
                     .allowPriorityChannels(false)
@@ -417,7 +417,7 @@
         assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
         assertTrue(ZenModeConfig.areAllZenBehaviorSoundsMuted(config));
 
-        config.areChannelsBypassingDnd = true;
+        config.hasPriorityChannels = true;
         if (Flags.modesUi()) {
             config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
                     .allowPriorityChannels(true)
@@ -429,7 +429,7 @@
         assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
         assertFalse(ZenModeConfig.areAllZenBehaviorSoundsMuted(config));
 
-        config.areChannelsBypassingDnd = false;
+        config.hasPriorityChannels = false;
         if (Flags.modesUi()) {
             config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
                     .allowPriorityChannels(false)
@@ -488,33 +488,33 @@
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.zenPolicy = null;
         rule.zenDeviceEffects = null;
-        assertThat(rule.canBeUpdatedByApp()).isTrue();
+        assertThat(rule.isUserModified()).isFalse();
 
         rule.userModifiedFields = 1;
 
-        assertThat(rule.canBeUpdatedByApp()).isFalse();
+        assertThat(rule.isUserModified()).isTrue();
     }
 
     @Test
     public void testCanBeUpdatedByApp_policyModified() throws Exception {
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.zenPolicy = new ZenPolicy();
-        assertThat(rule.canBeUpdatedByApp()).isTrue();
+        assertThat(rule.isUserModified()).isFalse();
 
         rule.zenPolicyUserModifiedFields = 1;
 
-        assertThat(rule.canBeUpdatedByApp()).isFalse();
+        assertThat(rule.isUserModified()).isTrue();
     }
 
     @Test
     public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build();
-        assertThat(rule.canBeUpdatedByApp()).isTrue();
+        assertThat(rule.isUserModified()).isFalse();
 
         rule.zenDeviceEffectsUserModifiedFields = 1;
 
-        assertThat(rule.canBeUpdatedByApp()).isFalse();
+        assertThat(rule.isUserModified()).isTrue();
     }
 
     @Test
@@ -548,7 +548,6 @@
         rule.creationTime = 123;
         rule.id = "id";
         rule.zenMode = INTERRUPTION_FILTER;
-        rule.modified = true;
         rule.name = NAME;
         rule.setConditionOverride(OVERRIDE_DEACTIVATE);
         rule.pkg = OWNER.getPackageName();
@@ -564,6 +563,9 @@
         rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
         if (Flags.modesUi()) {
             rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+            if (Flags.modesCleanupImplicit()) {
+                rule.lastActivation = Instant.ofEpochMilli(456);
+            }
         }
         config.automaticRules.put(rule.id, rule);
 
@@ -585,7 +587,6 @@
         assertEquals(rule.condition, ruleActual.condition);
         assertEquals(rule.enabled, ruleActual.enabled);
         assertEquals(rule.creationTime, ruleActual.creationTime);
-        assertEquals(rule.modified, ruleActual.modified);
         assertEquals(rule.conditionId, ruleActual.conditionId);
         assertEquals(rule.name, ruleActual.name);
         assertEquals(rule.zenMode, ruleActual.zenMode);
@@ -602,6 +603,9 @@
         assertEquals(rule.deletionInstant, ruleActual.deletionInstant);
         if (Flags.modesUi()) {
             assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin);
+            if (Flags.modesCleanupImplicit()) {
+                assertEquals(rule.lastActivation, ruleActual.lastActivation);
+            }
         }
         if (Flags.backupRestoreLogging()) {
             verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2);
@@ -620,7 +624,6 @@
         rule.creationTime = 123;
         rule.id = "id";
         rule.zenMode = INTERRUPTION_FILTER;
-        rule.modified = true;
         rule.name = NAME;
         rule.setConditionOverride(OVERRIDE_DEACTIVATE);
         rule.pkg = OWNER.getPackageName();
@@ -636,6 +639,9 @@
         rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
         if (Flags.modesUi()) {
             rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+            if (Flags.modesCleanupImplicit()) {
+                rule.lastActivation = Instant.ofEpochMilli(789);
+            }
         }
 
         Parcel parcel = Parcel.obtain();
@@ -651,7 +657,6 @@
         assertEquals(rule.condition, parceled.condition);
         assertEquals(rule.enabled, parceled.enabled);
         assertEquals(rule.creationTime, parceled.creationTime);
-        assertEquals(rule.modified, parceled.modified);
         assertEquals(rule.conditionId, parceled.conditionId);
         assertEquals(rule.name, parceled.name);
         assertEquals(rule.zenMode, parceled.zenMode);
@@ -668,6 +673,9 @@
         assertEquals(rule.deletionInstant, parceled.deletionInstant);
         if (Flags.modesUi()) {
             assertEquals(rule.disabledOrigin, parceled.disabledOrigin);
+            if (Flags.modesCleanupImplicit()) {
+                assertEquals(rule.lastActivation, parceled.lastActivation);
+            }
         }
 
         assertEquals(rule, parceled);
@@ -685,7 +693,6 @@
         rule.creationTime = 123;
         rule.id = "id";
         rule.zenMode = Settings.Global.ZEN_MODE_ALARMS;
-        rule.modified = true;
         rule.name = "name";
         rule.snoozing = true;
         rule.pkg = "b";
@@ -705,7 +712,6 @@
         assertEquals(rule.condition, fromXml.condition);
         assertEquals(rule.enabled, fromXml.enabled);
         assertEquals(rule.creationTime, fromXml.creationTime);
-        assertEquals(rule.modified, fromXml.modified);
         assertEquals(rule.conditionId, fromXml.conditionId);
         assertEquals(rule.name, fromXml.name);
         assertEquals(rule.zenMode, fromXml.zenMode);
@@ -721,7 +727,6 @@
         rule.enabled = ENABLED;
         rule.id = "id";
         rule.zenMode = INTERRUPTION_FILTER;
-        rule.modified = true;
         rule.name = NAME;
         rule.setConditionOverride(OVERRIDE_DEACTIVATE);
         rule.pkg = OWNER.getPackageName();
@@ -753,6 +758,9 @@
         rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
         if (Flags.modesUi()) {
             rule.disabledOrigin = ZenModeConfig.ORIGIN_APP;
+            if (Flags.modesCleanupImplicit()) {
+                rule.lastActivation = Instant.ofEpochMilli(123);
+            }
         }
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -770,7 +778,6 @@
         assertEquals(rule.condition, fromXml.condition);
         assertEquals(rule.enabled, fromXml.enabled);
         assertEquals(rule.creationTime, fromXml.creationTime);
-        assertEquals(rule.modified, fromXml.modified);
         assertEquals(rule.conditionId, fromXml.conditionId);
         assertEquals(rule.name, fromXml.name);
         assertEquals(rule.zenMode, fromXml.zenMode);
@@ -789,6 +796,9 @@
         assertEquals(rule.deletionInstant, fromXml.deletionInstant);
         if (Flags.modesUi()) {
             assertEquals(rule.disabledOrigin, fromXml.disabledOrigin);
+            if (Flags.modesCleanupImplicit()) {
+                assertEquals(rule.lastActivation, fromXml.lastActivation);
+            }
         }
     }
 
@@ -916,7 +926,7 @@
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
         assertThat(rule.userModifiedFields).isEqualTo(1);
-        assertThat(rule.canBeUpdatedByApp()).isFalse();
+        assertThat(rule.isUserModified()).isTrue();
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         writeRuleXml(rule, baos);
@@ -924,7 +934,7 @@
         ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
 
         assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields);
-        assertThat(fromXml.canBeUpdatedByApp()).isFalse();
+        assertThat(fromXml.isUserModified()).isTrue();
     }
 
     @Test
@@ -1259,7 +1269,6 @@
         rule.creationTime = 123;
         rule.id = "id";
         rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        rule.modified = true;
         rule.name = "name";
         rule.pkg = "b";
         config.automaticRules.put("key", rule);
@@ -1348,7 +1357,7 @@
             config.setSuppressedVisualEffects(0);
             config.setAllowPriorityChannels(false);
         }
-        config.areChannelsBypassingDnd = false;
+        config.hasPriorityChannels = false;
 
         return config;
     }
@@ -1383,7 +1392,7 @@
             config.setSuppressedVisualEffects(0);
             config.setAllowPriorityChannels(true);
         }
-        config.areChannelsBypassingDnd = false;
+        config.hasPriorityChannels = false;
         return config;
     }
 
@@ -1410,7 +1419,7 @@
             config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
             config.setSuppressedVisualEffects(0);
         }
-        config.areChannelsBypassingDnd = false;
+        config.hasPriorityChannels = false;
         return config;
     }
 
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 b138c72..6d0bf8b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.notification;
 
-import static android.app.Flags.FLAG_MODES_API;
 import static android.app.Flags.FLAG_MODES_UI;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -64,7 +63,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Optional;
@@ -78,20 +76,14 @@
     // version is not included in the diff; manual & automatic rules have special handling;
     // deleted rules are not included in the diff.
     public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS =
-            android.app.Flags.modesApi()
-                    ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
-                    : Set.of("version", "manualRule", "automaticRules");
-
-    // allowPriorityChannels is flagged by android.app.modes_api
-    public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
-            Set.of("allowPriorityChannels");
+            Set.of("version", "manualRule", "automaticRules", "deletedRules");
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return FlagsParameterization.progressionOf(FLAG_MODES_API, FLAG_MODES_UI);
+        return FlagsParameterization.progressionOf(FLAG_MODES_UI);
     }
 
     public ZenModeDiffTest(FlagsParameterization flags) {
@@ -147,7 +139,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testRuleDiff_toStringNoChangeAddRemove() throws Exception {
         // Start with two identical rules
         ZenModeConfig.ZenRule r1 = makeRule();
@@ -164,7 +156,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testRuleDiff_toString() throws Exception {
         // Start with two identical rules
         ZenModeConfig.ZenRule r1 = makeRule();
@@ -218,7 +210,6 @@
                 + "mPriorityCalls:2->1, "
                 + "mConversationSenders:2->1, "
                 + "mAllowChannels:2->1}, "
-                + "modified:true->false, "
                 + "pkg:string1->string2, "
                 + "zenDeviceEffects:ZenDeviceEffectsDiff{"
                 + "mGrayscale:true->false, "
@@ -241,7 +232,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testRuleDiff_toStringNullStartPolicy() throws Exception {
         // Start with two identical rules
         ZenModeConfig.ZenRule r1 = makeRule();
@@ -278,7 +269,6 @@
                 + "creationTime:200->100, "
                 + "enabler:string1->string2, "
                 + "zenPolicy:ZenPolicyDiff{added}, "
-                + "modified:true->false, "
                 + "pkg:string1->string2, "
                 + "zenDeviceEffects:ZenDeviceEffectsDiff{added}, "
                 + "triggerDescription:string1->string2, "
@@ -485,16 +475,10 @@
         // "Metadata" fields are never compared.
         Set<String> exemptFields = new LinkedHashSet<>(
                 Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
-                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin"));
+                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin",
+                        "lastActivation"));
         // Flagged fields are only compared if their flag is on.
-        if (!Flags.modesApi()) {
-            exemptFields.addAll(
-                    Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
-                            RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
-                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS,
-                            RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS));
-        }
-        if (Flags.modesApi() && Flags.modesUi()) {
+        if (Flags.modesUi()) {
             exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete.
         } else {
             exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE);
@@ -530,35 +514,6 @@
         ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
         ArrayMap<String, Object> expectedTo = new ArrayMap<>();
         List<Field> fieldsForDiff = getFieldsForDiffCheck(
-                ZenModeConfig.class, getConfigExemptAndFlaggedFields(), false);
-        generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
-
-        ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
-        assertTrue(d.hasDiff());
-
-        // Now diff them and check that each of the fields has a diff
-        for (Field f : fieldsForDiff) {
-            String name = f.getName();
-            assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
-            assertTrue(d.getDiffForField(name).hasDiff());
-            assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
-            assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
-            assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
-            assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
-        }
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
-    public void testConfigDiff_fieldDiffs_flagOn() throws Exception {
-        // these two start the same
-        ZenModeConfig c1 = new ZenModeConfig();
-        ZenModeConfig c2 = new ZenModeConfig();
-
-        // maps mapping field name -> expected output value as we set diffs
-        ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
-        ArrayMap<String, Object> expectedTo = new ArrayMap<>();
-        List<Field> fieldsForDiff = getFieldsForDiffCheck(
                 ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS, false);
         generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
 
@@ -656,14 +611,6 @@
         assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to());
     }
 
-    // Helper method that merges the base exempt fields with fields that are flagged
-    private Set getConfigExemptAndFlaggedFields() {
-        Set merged = new HashSet();
-        merged.addAll(ZEN_MODE_CONFIG_EXEMPT_FIELDS);
-        merged.addAll(ZEN_MODE_CONFIG_FLAGGED_FIELDS);
-        return merged;
-    }
-
     // Helper methods for working with configs, policies, rules
     // Just makes a zen rule with fields filled in
     private ZenModeConfig.ZenRule makeRule() {
@@ -676,20 +623,17 @@
         rule.creationTime = 123;
         rule.id = "ruleId";
         rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        rule.modified = false;
         rule.name = "name";
         rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE);
         rule.pkg = "a";
-        if (android.app.Flags.modesApi()) {
-            rule.allowManualInvocation = true;
-            rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
-            rule.iconResName = "res";
-            rule.triggerDescription = "At night";
-            rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
-                    .setShouldDimWallpaper(true)
-                    .build();
-            rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
-        }
+        rule.allowManualInvocation = true;
+        rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+        rule.iconResName = "res";
+        rule.triggerDescription = "At night";
+        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/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
index a49f5a8..2f0b3ec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
@@ -37,7 +37,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import android.app.Flags;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager.Policy;
@@ -492,8 +491,6 @@
 
     @Test
     public void testAllowChannels_priorityPackage() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         // Notification with package priority = PRIORITY_MAX (assigned to indicate canBypassDnd)
         NotificationRecord r = getNotificationRecord();
         r.setPackagePriority(Notification.PRIORITY_MAX);
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 31b9cf72..0ab11e0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -23,7 +23,7 @@
 import static android.app.AutomaticZenRule.TYPE_THEATER;
 import static android.app.AutomaticZenRule.TYPE_UNKNOWN;
 import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING;
-import static android.app.Flags.FLAG_MODES_API;
+import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT;
 import static android.app.Flags.FLAG_MODES_MULTIUSER;
 import static android.app.Flags.FLAG_MODES_UI;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED;
@@ -85,6 +85,7 @@
 import static android.service.notification.ZenPolicy.VISUAL_EFFECT_PEEK;
 
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
+import static com.android.os.dnd.DNDProtoEnums.CONV_IMPORTANT;
 import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
 import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
 import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
@@ -124,7 +125,10 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
+import static java.time.temporal.ChronoUnit.DAYS;
+
 import android.Manifest;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AlarmManager;
@@ -219,11 +223,11 @@
 import java.io.StringWriter;
 import java.time.Instant;
 import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -713,7 +717,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testTotalSilence_consolidatedPolicyDisallowsAll() {
         // Start with zen mode off just to make sure global/manual mode isn't doing anything.
         mZenModeHelper.mZenMode = ZEN_MODE_OFF;
@@ -746,7 +749,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testAlarmsOnly_consolidatedPolicyOnlyAllowsAlarmsAndMedia() {
         // Start with zen mode off just to make sure global/manual mode isn't doing anything.
         mZenModeHelper.mZenMode = ZEN_MODE_OFF;
@@ -1136,8 +1138,7 @@
     @Test
     public void testProto() throws InvalidProtocolBufferException {
         mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, null,
-                "test", CUSTOM_PKG_UID);
+                ORIGIN_USER_IN_SYSTEMUI, null, "test", CUSTOM_PKG_UID);
 
         mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); // no automatic rules
 
@@ -1262,7 +1263,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testProtoWithAutoRuleCustomPolicy() throws Exception {
         setupZenConfig();
         // clear any automatic rules just to make sure
@@ -1304,7 +1304,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testProtoWithAutoRuleWithModifiedFields() throws Exception {
         setupZenConfig();
         mZenModeHelper.mConfig.automaticRules = new ArrayMap<>();
@@ -2005,7 +2004,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testReadXml_onModesApi_noUpgrade() throws Exception {
         // When reading XML for something that is already on the modes API system, make sure no
         // rules' policies get changed.
@@ -2053,7 +2051,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception {
         // When reading in an XML file written from a pre-modes-API version, confirm that we create
         // a custom policy matching the global config for any automatic rule with no specified
@@ -2105,7 +2102,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception {
         // When reading in an XML file written from a pre-modes-API version, confirm that for an
         // underspecified ZenPolicy, we fill in all of the gaps with things from the global config
@@ -2165,7 +2161,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy()
             throws Exception {
         setupZenConfig();
@@ -2227,7 +2222,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testReadXml_upgradeToModesUi_resetsImplicitRuleIcon() throws Exception {
         setupZenConfig();
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -2241,13 +2236,12 @@
         mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id,
                 implicitRuleBeforeModesUi);
         // Plus one other normal rule.
-        ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
-        anotherRule.id = "other_rule";
+        ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now());
         anotherRule.iconResName = "other_icon";
         anotherRule.type = TYPE_IMMERSIVE;
         mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
 
-        // Write with pre-modes-ui = (modes_api) version, then re-read.
+        // Write with pre-modes-ui version, then re-read.
         ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_API);
         TypedXmlPullParser parser = Xml.newFastPullParser();
         parser.setInput(new BufferedInputStream(
@@ -2265,7 +2259,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testReadXml_onModesUi_implicitRulesUntouched() throws Exception {
         setupZenConfig();
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -2279,8 +2273,7 @@
                 implicitRuleWithModesUi);
 
         // Plus one other normal rule.
-        ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
-        anotherRule.id = "other_rule";
+        ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now());
         anotherRule.iconResName = "other_icon";
         anotherRule.type = TYPE_IMMERSIVE;
         mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
@@ -2342,7 +2335,7 @@
 
         // shouldn't update rule that's been modified
         ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
-        updatedDefaultRule.modified = true;
+        updatedDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         updatedDefaultRule.enabled = false;
         updatedDefaultRule.creationTime = 0;
         updatedDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
@@ -2370,8 +2363,8 @@
         // will update rule that is not enabled and modified
         ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule();
         customDefaultRule.pkg = SystemZenRules.PACKAGE_ANDROID;
+        customDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
         customDefaultRule.enabled = false;
-        customDefaultRule.modified = false;
         customDefaultRule.creationTime = 0;
         customDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
         customDefaultRule.name = "Schedule Default Rule";
@@ -2391,7 +2384,7 @@
         ZenModeConfig.ZenRule ruleAfterUpdating =
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID);
         assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled);
-        assertEquals(customDefaultRule.modified, ruleAfterUpdating.modified);
+        assertEquals(customDefaultRule.userModifiedFields, ruleAfterUpdating.userModifiedFields);
         assertEquals(customDefaultRule.id, ruleAfterUpdating.id);
         assertEquals(customDefaultRule.conditionId, ruleAfterUpdating.conditionId);
         assertNotEquals(defaultRuleName, ruleAfterUpdating.name); // update name
@@ -2401,8 +2394,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
-    public void testDefaultRulesFromConfig_modesApi_getPolicies() {
+    public void testDefaultRulesFromConfig_getPolicies() {
         // After mZenModeHelper was created, set some things in the policy so it's changed from
         // default.
         setupZenConfig();
@@ -2530,7 +2522,6 @@
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
         assertTrue(ruleInConfig != null);
         assertEquals(zenRule.isEnabled(), ruleInConfig.enabled);
-        assertEquals(zenRule.isModified(), ruleInConfig.modified);
         assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId);
         assertEquals(NotificationManager.zenModeFromInterruptionFilter(
                 zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode);
@@ -2551,7 +2542,6 @@
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
         assertTrue(ruleInConfig != null);
         assertEquals(zenRule.isEnabled(), ruleInConfig.enabled);
-        assertEquals(zenRule.isModified(), ruleInConfig.modified);
         assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId);
         assertEquals(NotificationManager.zenModeFromInterruptionFilter(
                 zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode);
@@ -2560,8 +2550,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
-    public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() {
+    public void testAddAutomaticZenRule_fillsInDefaultValues() {
         // When a new automatic zen rule is added with only some fields filled in, ensure that
         // all unset fields are filled in with device defaults.
 
@@ -2762,7 +2751,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_fromApp_ignoresHiddenEffects() {
         ZenDeviceEffects zde =
                 new ZenDeviceEffects.Builder()
@@ -2799,7 +2787,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_fromSystem_respectsHiddenEffects() {
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
                 .setShouldDisplayGrayscale(true)
@@ -2828,7 +2815,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception {
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
                 .setShouldDisplayGrayscale(true)
@@ -2859,7 +2845,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromApp_preservesPreviousHiddenEffects() {
         ZenDeviceEffects original = new ZenDeviceEffects.Builder()
                 .setShouldDisableTapToWake(true)
@@ -2896,7 +2881,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromSystem_updatesHiddenEffects() {
         ZenDeviceEffects original = new ZenDeviceEffects.Builder()
                 .setShouldDisableTapToWake(true)
@@ -2925,7 +2909,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromUser_updatesHiddenEffects() {
         ZenDeviceEffects original = new ZenDeviceEffects.Builder()
                 .setShouldDisableTapToWake(true)
@@ -2958,7 +2941,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_nullPolicy_doesNothing() {
         // Test that when updateAutomaticZenRule is called with a null policy, nothing changes
         // about the existing policy.
@@ -2985,7 +2967,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_overwritesExistingPolicy() {
         // Test that when updating an automatic zen rule with an existing policy, the newly set
         // fields overwrite those from the previous policy, but unset fields in the new policy
@@ -3024,7 +3005,6 @@
 
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
         ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                 ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3044,7 +3024,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_withTypeBedtime_keepsEnabledSleeping() {
         ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                 ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3065,7 +3044,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_withTypeBedtime_keepsCustomizedSleeping() {
         ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                 ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3086,7 +3064,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
         ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                 ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3113,7 +3091,34 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
+    public void getAutomaticZenRules_returnsOwnedRules() {
+        AutomaticZenRule myRule1 = new AutomaticZenRule.Builder("My Rule 1", Uri.parse("1"))
+                .setPackage(mPkg)
+                .setConfigurationActivity(new ComponentName(mPkg, "myActivity"))
+                .build();
+        AutomaticZenRule myRule2 = new AutomaticZenRule.Builder("My Rule 2", Uri.parse("2"))
+                .setPackage(mPkg)
+                .setConfigurationActivity(new ComponentName(mPkg, "myActivity"))
+                .build();
+        AutomaticZenRule otherPkgRule = new AutomaticZenRule.Builder("Other", Uri.parse("3"))
+                .setPackage("com.other.package")
+                .setConfigurationActivity(new ComponentName("com.other.package", "theirActivity"))
+                .build();
+
+        String rule1Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule1,
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        String rule2Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule2,
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        String otherRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
+                "com.other.package", otherPkgRule, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+        Map<String, AutomaticZenRule> rules = mZenModeHelper.getAutomaticZenRules(
+                UserHandle.CURRENT, CUSTOM_PKG_UID);
+
+        assertThat(rules.keySet()).containsExactly(rule1Id, rule2Id);
+    }
+
+    @Test
     public void testSetManualZenMode() {
         setupZenConfig();
 
@@ -3133,7 +3138,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     @DisableFlags(FLAG_MODES_UI)
     public void setManualZenMode_off_snoozesActiveRules() {
         for (ZenChangeOrigin origin : ZenChangeOrigin.values()) {
@@ -3172,7 +3176,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setManualZenMode_off_doesNotSnoozeRulesIfFromUserInSystemUi() {
         for (ZenChangeOrigin origin : ZenChangeOrigin.values()) {
             // Start with an active rule and an inactive rule
@@ -3246,7 +3250,7 @@
         // Turn zen mode on (to important_interruptions)
         // Need to additionally call the looper in order to finish the post-apply-config process
         mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", null, SYSTEM_UID);
+                ORIGIN_USER_IN_SYSTEMUI, "", null, SYSTEM_UID);
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
@@ -3273,14 +3277,13 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
-        assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
-                !(Flags.modesUi() || Flags.modesApi()));
+        assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isFalse();
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
         // change origin should be populated only under modes_ui
         assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo(
-                (Flags.modesApi() && Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0);
+                (Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0);
 
         // and from turning zen mode off:
         //   - event ID: DND_TURNED_OFF
@@ -3298,11 +3301,7 @@
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
-        if (Flags.modesApi()) {
-            assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
-        } else {
-            checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
-        }
+        assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
         assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
                 Flags.modesUi() ? ORIGIN_APP : 0);
     }
@@ -3334,8 +3333,7 @@
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule,
-                Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "",
-                SYSTEM_UID);
+                ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID);
 
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
                 null,
@@ -3345,8 +3343,7 @@
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String systemId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(), systemRule,
-                Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "test",
-                SYSTEM_UID);
+                ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID);
 
         // Event 3: turn on the system rule
         mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, systemId,
@@ -3355,8 +3352,7 @@
 
         // Event 4: "User" deletes the rule
         mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, systemId,
-                Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "",
-                SYSTEM_UID);
+                ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID);
         // In total, this represents 4 events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
 
@@ -3394,11 +3390,7 @@
         assertTrue(mZenModeEventLogger.getIsUserAction(1));
         assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(
                 Flags.modesUi() ? CUSTOM_PKG_UID : SYSTEM_UID);
-        if (Flags.modesApi()) {
-            assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
-        } else {
-            checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
-        }
+        assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
         assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
                 Flags.modesUi() ? ORIGIN_USER_IN_SYSTEMUI : 0);
 
@@ -3426,7 +3418,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser()
             throws IllegalArgumentException {
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
@@ -3816,13 +3807,13 @@
 
         // Now change apps bypassing to true
         ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
-        newConfig.areChannelsBypassingDnd = true;
+        newConfig.hasPriorityChannels = true;
         mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(),
                 ORIGIN_SYSTEM, SYSTEM_UID);
         assertEquals(2, mZenModeEventLogger.numLoggedChanges());
 
         // and then back to false, all without changing anything else
-        newConfig.areChannelsBypassingDnd = false;
+        newConfig.hasPriorityChannels = false;
         mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(),
                 ORIGIN_SYSTEM, SYSTEM_UID);
         assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -3869,10 +3860,9 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testZenModeEventLog_policyAllowChannels() {
-        // when modes_api flag is on, ensure that any change in allow_channels gets logged,
-        // even when there are no other changes.
+        // Ensure that any change in allow_channels gets logged, even when there are no other
+        // changes.
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
 
         // Default zen config has allow channels = priority (aka on)
@@ -3919,7 +3909,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testZenModeEventLog_ruleWithInterruptionFilterAll_notLoggedAsDndChange() {
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
@@ -3961,7 +3950,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testZenModeEventLog_activeRuleTypes() {
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
@@ -4050,43 +4038,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() {
-        setupZenConfig();
-        // When there's one automatic rule active and it doesn't specify a policy, test that the
-        // resulting consolidated policy is one that matches the default rule settings.
-        AutomaticZenRule zenRule = new AutomaticZenRule("name",
-                null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
-                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                null,
-                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-        String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
-        // enable the rule
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
-                new Condition(zenRule.getConditionId(), "", STATE_TRUE),
-                ORIGIN_SYSTEM, SYSTEM_UID);
-
-        assertEquals(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT),
-                mZenModeHelper.getConsolidatedNotificationPolicy());
-
-        // inspect the consolidated policy. Based on setupZenConfig() values.
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms());
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia());
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem());
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders());
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowCalls());
-        assertEquals(PRIORITY_SENDERS_STARRED, mZenModeHelper.mConsolidatedPolicy.allowCallsFrom());
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages());
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations());
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers());
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges());
-    }
-
-    @Test
-    public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault() {
+    public void testUpdateConsolidatedPolicy_defaultRulesOnly_takesDefault() {
         setupZenConfig();
 
         // When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -4113,53 +4065,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() {
-        setupZenConfig();
-
-        // when there's only one automatic rule active and it has a custom policy, make sure that's
-        // what the consolidated policy reflects whether or not it's stricter than what the global
-        // config would specify.
-        ZenPolicy customPolicy = new ZenPolicy.Builder()
-                .allowAlarms(true)  // more lenient than default
-                .allowMedia(true)  // more lenient than default
-                .allowRepeatCallers(false)  // more restrictive than default
-                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)  // more restrictive than default
-                .showBadges(true)  // more lenient
-                .showPeeking(false)  // more restrictive
-                .build();
-
-        AutomaticZenRule zenRule = new AutomaticZenRule("name",
-                null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
-                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                customPolicy,
-                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-        String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
-        // enable the rule; this will update the consolidated policy
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
-                new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID);
-
-        // since this is the only active rule, the consolidated policy should match the custom
-        // policy for every field specified, and take default values (from device default or
-        // manual policy) for unspecified things
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms());  // custom
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia());  // custom
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem());  // default
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders());  // default
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls());  // custom
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations());  // default
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers());  // custom
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.showBadges());  // custom
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking());  // custom
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault() {
+    public void testUpdateConsolidatedPolicy_customPolicyOnly_fillInWithDefault() {
         setupZenConfig();
 
         // when there's only one automatic rule active and it has a custom policy, make sure that's
@@ -4204,68 +4110,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() {
-        setupZenConfig();
-
-        // when there are two rules active, one inheriting the default policy and one setting its
-        // own custom policy, they should be merged to form the most restrictive combination.
-
-        // rule 1: no custom policy
-        AutomaticZenRule zenRule = new AutomaticZenRule("name",
-                null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
-                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                null,
-                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-        String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
-        // enable rule 1
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
-                new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID);
-
-        // custom policy for rule 2
-        ZenPolicy customPolicy = new ZenPolicy.Builder()
-                .allowAlarms(true)  // more lenient than default
-                .allowMedia(true)  // more lenient than default
-                .allowRepeatCallers(false)  // more restrictive than default
-                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)  // more restrictive than default
-                .showBadges(true)  // more lenient
-                .showPeeking(false)  // more restrictive
-                .build();
-
-        AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
-                null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
-                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                customPolicy,
-                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-        String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
-        // enable rule 2; this will update the consolidated policy
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2,
-                new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
-                ORIGIN_SYSTEM, SYSTEM_UID);
-
-        // now both rules should be on, and the consolidated policy should reflect the most
-        // restrictive option of each of the two
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms());  // default stricter
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia());  // default stricter
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem());  // default, unset in custom
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders());  // default
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls());  // custom stricter
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default, unset in custom
-        assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations());  // default
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers());  // custom stricter
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges());  // default stricter
-        assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking());  // custom stricter
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+    public void testUpdateConsolidatedPolicy_defaultAndCustomActive_mergesWithDefault() {
         setupZenConfig();
 
         // when there are two rules active, one inheriting the default policy and one setting its
@@ -4328,7 +4173,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testUpdateConsolidatedPolicy_allowChannels() {
         setupZenConfig();
 
@@ -4377,7 +4221,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() {
         setupZenConfig();
 
@@ -4428,7 +4271,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void zenRuleToAutomaticZenRule_allFields() {
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
                 new String[]{OWNER.getPackageName()});
@@ -4442,7 +4284,6 @@
         rule.creationTime = 123;
         rule.id = "id";
         rule.zenMode = INTERRUPTION_FILTER_ZR;
-        rule.modified = true;
         rule.name = NAME;
         rule.setConditionOverride(OVERRIDE_DEACTIVATE);
         rule.pkg = OWNER.getPackageName();
@@ -4473,7 +4314,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void automaticZenRuleToZenRule_allFields() {
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
                 new String[]{OWNER.getPackageName()});
@@ -4515,7 +4355,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() {
         // Add a starting rule with the name OriginalName.
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID)
@@ -4573,7 +4412,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() {
         // Adds a starting rule with empty zen policies and device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4627,7 +4465,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromSystemUi_updatesValues() {
         // Adds a starting rule with empty zen policies and device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4678,7 +4515,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() {
         // Adds a starting rule with empty zen policies and device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4754,7 +4590,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_updatesValues() {
         // Adds a starting rule with empty zen policies and device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4777,11 +4612,10 @@
         assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
 
         ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
-        assertThat(storedRule.canBeUpdatedByApp()).isTrue();
+        assertThat(storedRule.isUserModified()).isFalse();
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_nullDeviceEffectsUpdate() {
         // Adds a starting rule with empty zen policies and device effects
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build();
@@ -4809,7 +4643,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_nullPolicyUpdate() {
         // Adds a starting rule with set zen policy and empty device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4838,7 +4671,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() {
         when(mContext.checkCallingPermission(anyString()))
                 .thenReturn(PackageManager.PERMISSION_GRANTED);
@@ -4888,7 +4720,7 @@
                 STATE_DISALLOW);
 
         ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
-        assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+        assertThat(storedRule.isUserModified()).isTrue();
         assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
                 ZenPolicy.FIELD_ALLOW_CHANNELS
                         | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
@@ -4902,7 +4734,6 @@
     }
 
     @Test
-    @EnableFlags(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)
@@ -4931,7 +4762,7 @@
         assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
 
         ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
-        assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+        assertThat(storedRule.isUserModified()).isTrue();
         assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(
                 ZenDeviceEffects.FIELD_GRAYSCALE);
     }
@@ -5007,7 +4838,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testUpdateAutomaticRule_activated_triggersBroadcast() throws Exception {
         setupZenConfig();
 
@@ -5047,7 +4877,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testUpdateAutomaticRule_deactivatedByUser_triggersBroadcast() throws Exception {
         setupZenConfig();
 
@@ -5091,7 +4920,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testUpdateAutomaticRule_deactivatedByApp_triggersBroadcast() throws Exception {
         setupZenConfig();
 
@@ -5167,7 +4995,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_ruleChanged_deactivatesRule() {
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5191,7 +5018,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_ruleNotChanged_doesNotDeactivateRule() {
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5214,7 +5040,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule() {
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5239,7 +5065,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() {
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
@@ -5266,7 +5091,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() {
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5291,7 +5116,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_changeOwnerForSystemRule_allowed() {
         when(mContext.checkCallingPermission(anyString()))
                 .thenReturn(PackageManager.PERMISSION_GRANTED);
@@ -5314,7 +5139,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_changeOwnerForAppOwnedRule_ignored() {
         AutomaticZenRule original = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setOwner(new ComponentName(mContext.getPackageName(), "old.third.party.cps"))
@@ -5335,7 +5160,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         reset(mDeviceEffectsApplier);
@@ -5358,7 +5182,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_applied() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5384,7 +5207,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testSettingDeviceEffects_throwsExceptionIfAlreadySet() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
 
@@ -5394,7 +5216,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_onDeactivateRule_applied() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
 
@@ -5413,7 +5234,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_changeToConsolidatedEffects_applied() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5453,7 +5273,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5478,7 +5297,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_activeBeforeApplierProvided_appliedWhenProvided() {
         ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build();
         String ruleId = addRuleWithEffects(zde);
@@ -5494,7 +5312,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testDeviceEffects_onUserSwitch_appliedImmediately() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5522,7 +5339,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+    @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
     public void testDeviceEffects_allowsGrayscale() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         reset(mDeviceEffectsApplier);
@@ -5539,7 +5356,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+    @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
     public void testDeviceEffects_whileDriving_avoidsGrayscale() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         reset(mDeviceEffectsApplier);
@@ -5563,7 +5380,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+    @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
     public void testDeviceEffects_whileDrivingWithGrayscale_allowsGrayscale() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         reset(mDeviceEffectsApplier);
@@ -5594,7 +5411,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() {
         // Start with a rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5651,7 +5467,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() {
         // Start with a single rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5685,7 +5500,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() {
         // Start with a single rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5736,7 +5550,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() {
         // Start with a single rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5779,7 +5592,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void removeAndAddAutomaticZenRule_ifChangingComponent_isAllowedAndDoesNotRestore() {
         // Start with a rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5824,7 +5637,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() {
         mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
                 PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match.
@@ -5874,7 +5686,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAllZenRules_preservedForRestoring() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -5898,14 +5709,13 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
         // Start with deleted rules from 2 different packages.
         Instant now = Instant.ofEpochMilli(1701796461000L);
-        ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now);
-        ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now);
+        ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now);
+        ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now);
         mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule);
         mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule);
 
@@ -5918,7 +5728,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_wasActive_isRestoredAsInactive() {
         // Start with a rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -5968,7 +5777,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void removeAndAddAutomaticZenRule_wasSnoozed_isRestoredAsInactive() {
         // Start with a rule.
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -6023,12 +5831,11 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testRuleCleanup() throws Exception {
         Instant now = Instant.ofEpochMilli(1701796461000L);
-        Instant yesterday = now.minus(1, ChronoUnit.DAYS);
-        Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS);
-        Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS);
+        Instant yesterday = now.minus(1, DAYS);
+        Instant aWeekAgo = now.minus(7, DAYS);
+        Instant twoMonthsAgo = now.minus(60, DAYS);
         mTestClock.setNowMillis(now.toEpochMilli());
 
         when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt()))
@@ -6041,24 +5848,28 @@
         config.user = 42;
         mZenModeHelper.mConfigs.put(42, config);
         // okay rules (not deleted, package exists, with a range of creation dates).
-        config.automaticRules.put("ar1", newZenRule("good_pkg", now, null));
-        config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null));
-        config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null));
+        config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now));
+        config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday));
+        config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo));
         // newish rules for a missing package
-        config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null));
+        config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday));
         // oldish rules belonging to a missing package
-        config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null));
+        config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo));
         // rules deleted recently
-        config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday));
-        config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo));
+        config.deletedRules.put("del1",
+                newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday));
+        config.deletedRules.put("del2",
+                newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo));
         // rules deleted a long time ago
-        config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo));
+        config.deletedRules.put("del3",
+                newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo));
         // rules for a missing package, created recently and deleted recently
-        config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now));
+        config.deletedRules.put("del4", newDeletedZenRule("del4", "bad_pkg", yesterday, now));
         // rules for a missing package, created a long time ago and deleted recently
-        config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now));
+        config.deletedRules.put("del5", newDeletedZenRule("del5", "bad_pkg", twoMonthsAgo, now));
         // rules for a missing package, created a long time ago and deleted a long time ago
-        config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo));
+        config.deletedRules.put("del6",
+                newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo));
 
         mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
 
@@ -6068,20 +5879,120 @@
                 .containsExactly("del1", "del2", "del4");
     }
 
-    private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) {
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception {
+        Instant now = Instant.ofEpochMilli(1701796461000L);
+        Instant yesterday = now.minus(1, DAYS);
+        Instant aWeekAgo = now.minus(7, DAYS);
+        Instant twoMonthsAgo = now.minus(60, DAYS);
+        Instant aYearAgo = now.minus(365, DAYS);
+        mTestClock.setNowMillis(now.toEpochMilli());
+        when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo());
+
+        // Set up a config to be loaded, containing a bunch of implicit rules
+        ZenModeConfig config = new ZenModeConfig();
+        config.user = 42;
+        mZenModeHelper.mConfigs.put(42, config);
+        // used recently
+        ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday);
+        ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo);
+        config.automaticRules.put(usedRecently1.id, usedRecently1);
+        config.automaticRules.put(usedRecently2.id, usedRecently2);
+        // not used in a long time
+        ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo);
+        config.automaticRules.put(longUnused.id, longUnused);
+        // created a long time ago, before lastActivation tracking
+        ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null);
+        config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown);
+        // created a short time ago, before lastActivation tracking
+        ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null);
+        config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown);
+        // not used in a long time, but was customized by user
+        ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo);
+        longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS;
+        config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized);
+        // created a long time ago, before lastActivation tracking, and was customized by user
+        ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo,
+                null);
+        oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON;
+        config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id,
+                oldAndLastUsageUnknownAndCustomized);
+
+        mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
+
+        // The recently used OR modified OR last-used-unknown rules stay.
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_METADATA)
+                .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown,
+                        newAndLastUsageUnknown, longUnusedButCustomized,
+                        oldAndLastUsageUnknownAndCustomized);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception {
+        Instant now = Instant.ofEpochMilli(1701796461000L);
+        Instant aWeekAgo = now.minus(7, DAYS);
+        Instant aYearAgo = now.minus(365, DAYS);
+        mTestClock.setNowMillis(now.toEpochMilli());
+        when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo());
+
+        // Set up a config to be loaded, containing implicit rules.
+        ZenModeConfig config = new ZenModeConfig();
+        config.user = 42;
+        mZenModeHelper.mConfigs.put(42, config);
+        // with last activation known
+        ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo);
+        config.automaticRules.put(usedRecently.id, usedRecently);
+        // created a long time ago, with last activation unknown
+        ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null);
+        config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown);
+        // created a short time ago, with last activation unknown
+        ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null);
+        config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown);
+
+        mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
+
+        // All rules stayed.
+        usedRecently = getZenRule(usedRecently.id);
+        oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id);
+        newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id);
+
+        // The rules with an unknown last usage have been assigned a placeholder one.
+        assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo);
+        assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now);
+        assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now);
+    }
+
+    private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt,
+            @NonNull Instant deletedAt) {
+        ZenRule rule = newZenRule(id, pkg, createdAt);
+        rule.deletionInstant = deletedAt;
+        return rule;
+    }
+
+    private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt,
+            @Nullable Instant lastActivatedAt) {
+        ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt);
+        implicitRule.lastActivation = lastActivatedAt;
+        return implicitRule;
+    }
+
+    private static ZenRule newZenRule(String id, String pkg, Instant createdAt) {
         ZenRule rule = new ZenRule();
+        rule.id = id;
         rule.pkg = pkg;
         rule.creationTime = createdAt.toEpochMilli();
         rule.enabled = true;
-        rule.deletionInstant = deletedAt;
+        rule.deletionInstant = null;
         // Plus stuff so that isValidAutomaticRule() passes
-        rule.name = "A rule from " + pkg + " created on " + createdAt;
+        rule.name = "Rule " + id;
         rule.conditionId = Uri.parse(rule.name);
         return rule;
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void getAutomaticZenRuleState_ownedRule_returnsRuleState() {
         String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(),
@@ -6112,14 +6023,13 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() {
         // Assume existence of a system-owned rule that is currently ACTIVE.
-        ZenRule systemRule = newZenRule("android", Instant.now(), null);
+        ZenRule systemRule = newZenRule("systemRule", "android", Instant.now());
         systemRule.zenMode = ZEN_MODE_ALARMS;
         systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE);
         ZenModeConfig config = mZenModeHelper.mConfig.copy();
-        config.automaticRules.put("systemRule", systemRule);
+        config.automaticRules.put(systemRule.id, systemRule);
         mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
 
@@ -6128,15 +6038,14 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() {
         // Assume existence of an other-package-owned rule that is currently ACTIVE.
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
-        ZenRule otherRule = newZenRule("another.package", Instant.now(), null);
+        ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now());
         otherRule.zenMode = ZEN_MODE_ALARMS;
         otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE);
         ZenModeConfig config = mZenModeHelper.mConfig.copy();
-        config.automaticRules.put("otherRule", otherRule);
+        config.automaticRules.put(otherRule.id, otherRule);
         mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
 
@@ -6149,15 +6058,14 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() {
         // Assume existence of an other-package-owned rule that is currently ACTIVE.
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
-        ZenRule otherRule = newZenRule("another.package", Instant.now(), null);
+        ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now());
         otherRule.zenMode = ZEN_MODE_ALARMS;
         otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE);
         ZenModeConfig config = mZenModeHelper.mConfig.copy();
-        config.automaticRules.put("otherRule", otherRule);
+        config.automaticRules.put(otherRule.id, otherRule);
         mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
 
@@ -6171,7 +6079,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testCallbacks_policy() throws Exception {
         setupZenConfig();
         assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowReminders())
@@ -6193,7 +6100,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void testCallbacks_consolidatedPolicy() throws Exception {
         assertThat(mZenModeHelper.getConsolidatedNotificationPolicy().allowMedia()).isTrue();
         SettableFuture<Policy> futureConsolidatedPolicy = SettableFuture.create();
@@ -6219,7 +6125,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6235,7 +6140,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6257,7 +6161,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
         String pkg = mContext.getPackageName();
@@ -6290,7 +6193,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
         String pkg = mContext.getPackageName();
@@ -6322,7 +6224,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
         mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, mPkg, CUSTOM_PKG_UID,
@@ -6339,7 +6240,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6350,7 +6250,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6375,7 +6274,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void applyGlobalZenModeAsImplicitZenRule_again_refreshesRuleName() {
         mZenModeHelper.mConfig.automaticRules.clear();
         mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
@@ -6394,7 +6293,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void applyGlobalZenModeAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
         mZenModeHelper.mConfig.automaticRules.clear();
         mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
@@ -6420,19 +6319,6 @@
     }
 
     @Test
-    @DisableFlags(FLAG_MODES_API)
-    public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
-        mZenModeHelper.mConfig.automaticRules.clear();
-
-        withoutWtfCrash(
-                () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
-                        CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS));
-
-        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6457,7 +6343,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
 
@@ -6489,7 +6374,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
         String pkg = mContext.getPackageName();
@@ -6532,7 +6416,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() {
         mZenModeHelper.mConfig.automaticRules.clear();
         String pkg = mContext.getPackageName();
@@ -6572,7 +6455,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void applyGlobalPolicyAsImplicitZenRule_again_refreshesRuleName() {
         mZenModeHelper.mConfig.automaticRules.clear();
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
@@ -6591,7 +6474,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void applyGlobalPolicyAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
         mZenModeHelper.mConfig.automaticRules.clear();
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
@@ -6617,19 +6500,6 @@
     }
 
     @Test
-    @DisableFlags(FLAG_MODES_API)
-    public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
-        mZenModeHelper.mConfig.automaticRules.clear();
-
-        withoutWtfCrash(
-                () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
-                        CUSTOM_PKG_NAME, CUSTOM_PKG_UID, new Policy(0, 0, 0)));
-
-        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
     public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() {
         Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
                 PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
@@ -6645,7 +6515,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     @DisableFlags(FLAG_MODES_UI)
     public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() {
         // Implicit rule will get the global policy at the time of rule creation.
@@ -6665,7 +6534,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() {
         Policy policy = new Policy(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_STARRED, 0);
         mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, policy, ORIGIN_APP,
@@ -6680,7 +6548,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     @DisableFlags(FLAG_MODES_UI)
     public void setNotificationPolicy_updatesRulePolicies_ifRulePolicyIsDefaultOrGlobalPolicy() {
         ZenPolicy defaultZenPolicy = mZenModeHelper.getDefaultZenPolicy();
@@ -6726,7 +6593,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
     public void addRule_iconIdWithResourceNameTooLong_ignoresIcon() {
         int resourceId = 999;
         String veryLongResourceName = "com.android.server.notification:drawable/"
@@ -6745,7 +6611,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setManualZenRuleDeviceEffects_noPreexistingMode() {
         ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
                 .setShouldDimWallpaper(true)
@@ -6759,7 +6625,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setManualZenRuleDeviceEffects_preexistingMode() {
         mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY,
                 ORIGIN_USER_IN_SYSTEMUI, "create manual rule", "settings", SYSTEM_UID);
@@ -6776,7 +6642,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void addAutomaticZenRule_startsDisabled_recordsDisabledOrigin() {
         AutomaticZenRule startsDisabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6792,7 +6658,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_disabling_recordsDisabledOrigin() {
         AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6815,7 +6681,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_keepingDisabled_preservesPreviousDisabledOrigin() {
         AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6845,7 +6711,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateAutomaticZenRule_enabling_clearsDisabledOrigin() {
         AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6875,7 +6741,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualActivation_appliesOverride() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -6893,7 +6759,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -6930,7 +6796,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualDeactivationAndThenReactivation_removesOverride() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -6976,7 +6842,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7002,7 +6868,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7039,7 +6905,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7085,7 +6951,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_withActivationOverride_userActionFromAppCanDeactivate() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7109,7 +6975,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_withDeactivationOverride_userActionFromAppCanActivate() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7140,7 +7006,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_manualActionFromApp_isNotOverride() {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
                 .setPackage(mPkg)
@@ -7165,7 +7031,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_implicitRuleManualActivation_doesNotUseOverride() {
         mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
                 PERMISSION_GRANTED); // So that canManageAZR succeeds.
@@ -7188,7 +7054,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_implicitRuleManualDeactivation_doesNotUseOverride() {
         mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
                 PERMISSION_GRANTED); // So that canManageAZR succeeds.
@@ -7214,24 +7080,8 @@
     }
 
     @Test
-    @DisableFlags({FLAG_MODES_API, FLAG_MODES_UI})
-    public void testDefaultConfig_preModesApi_rulesAreBare() {
-        // Create a new user, which should get a copy of the default policy.
-        mZenModeHelper.onUserSwitched(101);
-
-        ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get(
-                ZenModeConfig.EVENTS_OBSOLETE_RULE_ID);
-
-        assertThat(eventsRule).isNotNull();
-        assertThat(eventsRule.zenPolicy).isNull();
-        assertThat(eventsRule.type).isEqualTo(TYPE_UNKNOWN);
-        assertThat(eventsRule.triggerDescription).isNull();
-    }
-
-    @Test
-    @EnableFlags(FLAG_MODES_API)
     @DisableFlags(FLAG_MODES_UI)
-    public void testDefaultConfig_modesApi_rulesHaveFullPolicy() {
+    public void testDefaultConfig_rulesHaveFullPolicy() {
         // Create a new user, which should get a copy of the default policy.
         mZenModeHelper.onUserSwitched(201);
 
@@ -7245,7 +7095,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void testDefaultConfig_modesUi_rulesHaveFullPolicy() {
         // Create a new user, which should get a copy of the default policy.
         mZenModeHelper.onUserSwitched(301);
@@ -7260,7 +7110,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_withManualActivation_activeOnReboot()
             throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7298,7 +7148,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void setAutomaticZenRuleState_withManualDeactivation_clearedOnReboot()
             throws Exception {
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7336,7 +7186,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_MODES_API)
     public void addAutomaticZenRule_withoutPolicy_getsItsOwnInstanceOfDefaultPolicy() {
         // Add a rule without policy -> uses default config
         AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7352,7 +7201,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void readXml_withDisabledEventsRule_deletesIt() throws Exception {
         ZenRule rule = new ZenRule();
         rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID;
@@ -7372,7 +7221,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void readXml_withEnabledEventsRule_keepsIt() throws Exception {
         ZenRule rule = new ZenRule();
         rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID;
@@ -7392,7 +7241,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    @EnableFlags(FLAG_MODES_UI)
     public void updateHasPriorityChannels_keepsChannelSettings() {
         setupZenConfig();
 
@@ -7512,6 +7361,125 @@
                 "config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID);
     }
 
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void setAutomaticZenRuleState_updatesLastActivation() {
+        String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+                new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                        .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .build(),
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+                new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition"))
+                        .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .build(),
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleOne).lastActivation).isNull();
+        assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+        Instant firstActivation = Instant.ofEpochMilli(100);
+        mTestClock.setNow(firstActivation);
+        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE,
+                ORIGIN_APP, CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation);
+        assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+        mTestClock.setNow(Instant.ofEpochMilli(300));
+        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE,
+                ORIGIN_APP, CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation);
+        assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+        Instant secondActivation = Instant.ofEpochMilli(500);
+        mTestClock.setNow(secondActivation);
+        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE,
+                ORIGIN_APP, CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation);
+        assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void setManualZenMode_updatesLastActivation() {
+        assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull();
+        Instant instant = Instant.ofEpochMilli(100);
+        mTestClock.setNow(instant);
+
+        mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null,
+                ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID);
+
+        assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() {
+        Instant instant = Instant.ofEpochMilli(100);
+        mTestClock.setNow(instant);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME,
+                CUSTOM_PKG_UID, ZEN_MODE_ALARMS);
+
+        ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME));
+        assertThat(implicitRule.lastActivation).isEqualTo(instant);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() {
+        String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+                new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                        .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .build(),
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleId).lastActivation).isNull();
+
+        // Manual activation comes first
+        Instant firstActivation = Instant.ofEpochMilli(100);
+        mTestClock.setNow(firstActivation);
+        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE,
+                ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+
+        assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation);
+
+        // Now the app says the rule should be active (assume it's on a schedule, and the app
+        // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't
+        // change the activation state.
+        mTestClock.setNow(Instant.ofEpochMilli(300));
+        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE,
+                ORIGIN_APP, CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+    public void addOrUpdateRule_doesNotUpdateLastActivation() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+
+        String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr,
+                ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleId).lastActivation).isNull();
+
+        mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason",
+                CUSTOM_PKG_UID);
+
+        assertThat(getZenRule(ruleId).lastActivation).isNull();
+    }
+
     private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
             @Nullable ZenPolicy zenPolicy) {
         ZenRule rule = new ZenRule();
@@ -7529,22 +7497,27 @@
     }
 
     private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA =
-            Correspondence.transforming(zr -> {
-                        Parcel p = Parcel.obtain();
-                        try {
-                            zr.writeToParcel(p, 0);
-                            p.setDataPosition(0);
-                            ZenRule copy = new ZenRule(p);
-                            copy.creationTime = 0;
-                            copy.userModifiedFields = 0;
-                            copy.zenPolicyUserModifiedFields = 0;
-                            copy.zenDeviceEffectsUserModifiedFields = 0;
-                            return copy;
-                        } finally {
-                            p.recycle();
-                        }
-                    },
-                    "Ignoring timestamp and userModifiedFields");
+            Correspondence.transforming(
+                    ZenModeHelperTest::cloneWithoutMetadata,
+                    ZenModeHelperTest::cloneWithoutMetadata,
+                    "Ignoring timestamps and userModifiedFields");
+
+    private static ZenRule cloneWithoutMetadata(ZenRule rule) {
+        Parcel p = Parcel.obtain();
+        try {
+            rule.writeToParcel(p, 0);
+            p.setDataPosition(0);
+            ZenRule copy = new ZenRule(p);
+            copy.creationTime = 0;
+            copy.userModifiedFields = 0;
+            copy.zenPolicyUserModifiedFields = 0;
+            copy.zenDeviceEffectsUserModifiedFields = 0;
+            copy.lastActivation = null;
+            return copy;
+        } finally {
+            p.recycle();
+        }
+    }
 
     private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
             @Nullable Boolean conditionActive) {
@@ -7574,7 +7547,6 @@
         return rule;
     }
 
-    // TODO: b/310620812 - Update setup methods to include allowChannels() when MODES_API is inlined
     private void setupZenConfig() {
         Policy customPolicy = new Policy(PRIORITY_CATEGORY_REMINDERS
                 | PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_MESSAGES
@@ -7583,7 +7555,7 @@
                 PRIORITY_SENDERS_STARRED,
                 PRIORITY_SENDERS_STARRED,
                 SUPPRESSED_EFFECT_BADGE,
-                0,
+                0, // allows priority channels.
                 CONVERSATION_SENDERS_IMPORTANT);
         mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, customPolicy, ORIGIN_UNKNOWN, 1);
         if (!Flags.modesUi()) {
@@ -7599,8 +7571,11 @@
         assertEquals(STATE_ALLOW, dndProto.getCalls().getNumber());
         assertEquals(PEOPLE_STARRED, dndProto.getAllowCallsFrom().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getMessages().getNumber());
+        assertEquals(PEOPLE_STARRED, dndProto.getAllowMessagesFrom().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getEvents().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getRepeatCallers().getNumber());
+        assertEquals(STATE_ALLOW, dndProto.getConversations().getNumber());
+        assertEquals(CONV_IMPORTANT, dndProto.getAllowConversationsFrom().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getFullscreen().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getLights().getNumber());
         assertEquals(STATE_ALLOW, dndProto.getPeek().getNumber());
@@ -7616,7 +7591,7 @@
             return;
         }
 
-        // When modes_api flag is on, the default zen config is the device defaults.
+        // The default zen config is the device defaults.
         assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW);
         assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW);
         assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW);
@@ -7627,6 +7602,8 @@
         assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
         assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW);
         assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getConversations().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getAllowConversationsFrom().getNumber()).isEqualTo(CONV_IMPORTANT);
         assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW);
         assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW);
         assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW);
@@ -7634,6 +7611,8 @@
         assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW);
         assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW);
         assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getAllowChannels().getNumber()).isEqualTo(
+                DNDProtoEnums.CHANNEL_POLICY_PRIORITY);
     }
 
     private static String getZenLog() {
@@ -7642,16 +7621,6 @@
         return zenLogWriter.toString();
     }
 
-    private static void withoutWtfCrash(Runnable test) {
-        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {
-        });
-        try {
-            test.run();
-        } finally {
-            Log.setWtfHandler(oldHandler);
-        }
-    }
-
     /**
      * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml()
      */
@@ -7954,6 +7923,10 @@
             return mNowMillis;
         }
 
+        private void setNow(Instant instant) {
+            mNowMillis = instant.toEpochMilli();
+        }
+
         private void setNowMillis(long millis) {
             mNowMillis = millis;
         }
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 6433b76..8b93764 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -21,7 +21,6 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.fail;
 
-import android.app.Flags;
 import android.os.Parcel;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
@@ -34,7 +33,6 @@
 
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -50,11 +48,6 @@
     @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();
@@ -207,8 +200,6 @@
 
     @Test
     public void testZenPolicyApplyChannels_applyUnset() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         ZenPolicy unset = builder.build();
 
@@ -223,8 +214,6 @@
 
     @Test
     public void testZenPolicyApplyChannels_applyStricter() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         builder.allowPriorityChannels(false);
         ZenPolicy none = builder.build();
@@ -239,8 +228,6 @@
 
     @Test
     public void testZenPolicyApplyChannels_applyLooser() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         builder.allowPriorityChannels(false);
         ZenPolicy none = builder.build();
@@ -255,8 +242,6 @@
 
     @Test
     public void testZenPolicyApplyChannels_applySet() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         ZenPolicy unset = builder.build();
 
@@ -270,8 +255,6 @@
 
     @Test
     public void testZenPolicyOverwrite_allUnsetPolicies() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         ZenPolicy unset = builder.build();
 
@@ -292,8 +275,6 @@
 
     @Test
     public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         ZenPolicy p1 = new ZenPolicy.Builder()
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
                 .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
@@ -375,7 +356,6 @@
 
     @Test
     public void testEmptyZenPolicy_emptyChannels() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
 
         ZenPolicy policy = builder.build();
@@ -688,22 +668,7 @@
     }
 
     @Test
-    public void testAllowChannels_noFlag() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MODES_API);
-
-        // allowChannels should be unset, not be modifiable, and not show up in any output
-        ZenPolicy.Builder builder = new ZenPolicy.Builder();
-        builder.allowPriorityChannels(true);
-        ZenPolicy policy = builder.build();
-
-        assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
-        assertThat(policy.toString().contains("allowChannels")).isFalse();
-    }
-
-    @Test
     public void testAllowChannels() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
         // allow priority channels
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         builder.allowPriorityChannels(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 9e7575f..f5bda9f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -104,7 +104,6 @@
                 .setTask(mTrampolineActivity.getTask())
                 .setComponent(createRelative(DEFAULT_COMPONENT_PACKAGE_NAME, "TopActivity"))
                 .build();
-        mTopActivity.mDisplayContent.mOpeningApps.add(mTopActivity);
         mTransition = new Transition(TRANSIT_OPEN, 0 /* flags */,
                 mTopActivity.mTransitionController, createTestBLASTSyncEngine());
         mTransition.mParticipants.add(mTopActivity);
@@ -485,7 +484,6 @@
 
     @Test
     public void testActivityDrawnWithoutTransition() {
-        mTopActivity.mDisplayContent.mOpeningApps.remove(mTopActivity);
         mTransition.mParticipants.remove(mTopActivity);
         onIntentStarted(mTopActivity.intent);
         notifyAndVerifyActivityLaunched(mTopActivity);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index bb29614..280e432 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3326,16 +3326,13 @@
         makeWindowVisibleAndDrawn(app);
 
         // Put the activity in close transition.
-        mDisplayContent.mOpeningApps.clear();
-        mDisplayContent.mClosingApps.add(app.mActivityRecord);
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
+        requestTransition(app.mActivityRecord, WindowManager.TRANSIT_CLOSE);
 
         // Remove window during transition, so it is requested to hide, but won't be committed until
         // the transition is finished.
         app.mActivityRecord.onRemovedFromDisplay();
         app.mActivityRecord.prepareSurfaces();
 
-        assertTrue(mDisplayContent.mClosingApps.contains(app.mActivityRecord));
         assertFalse(app.mActivityRecord.isVisibleRequested());
         assertTrue(app.mActivityRecord.isVisible());
         assertTrue(app.mActivityRecord.isSurfaceShowing());
@@ -3353,11 +3350,6 @@
         makeWindowVisibleAndDrawn(app);
         app.mActivityRecord.prepareSurfaces();
 
-        // Put the activity in close transition.
-        mDisplayContent.mOpeningApps.clear();
-        mDisplayContent.mClosingApps.add(app.mActivityRecord);
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
-
         // Commit visibility before start transition.
         app.mActivityRecord.commitVisibility(false, false);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 3c74ad0..a9be47d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -509,6 +509,32 @@
         assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
     }
 
+    @Test
+    public void testOpaque_nonLeafTaskFragmentWithDirectActivity_opaque() {
+        final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+                .build();
+        directChildActivity.setOccludesParent(true);
+        final Task nonLeafTask = directChildActivity.getTask();
+        final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(),
+                true /* createdByOrganizer */, false /* isEmbedded */);
+        nonLeafTask.addChild(directChildFragment, 0);
+
+        assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isTrue();
+    }
+
+    @Test
+    public void testOpaque_nonLeafTaskFragmentWithDirectActivity_transparent() {
+        final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+                .build();
+        directChildActivity.setOccludesParent(false);
+        final Task nonLeafTask = directChildActivity.getTask();
+        final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(),
+                true /* createdByOrganizer */, false /* isEmbedded */);
+        nonLeafTask.addChild(directChildFragment, 0);
+
+        assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isFalse();
+    }
+
     @NonNull
     private TaskFragment createChildTaskFragment(@NonNull Task parent,
             @WindowConfiguration.WindowingMode int windowingMode,
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 018ea58..d016e735 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -151,6 +151,10 @@
         doReturn(taskBounds).when(mTaskStack.top()).getBounds();
     }
 
+    void configureTaskAppBounds(@NonNull Rect appBounds) {
+        mTaskStack.top().getWindowConfiguration().setAppBounds(appBounds);
+    }
+
     void configureTopActivityBounds(@NonNull Rect activityBounds) {
         doReturn(activityBounds).when(mActivityStack.top()).getBounds();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
index 0c31032..2603d78 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
@@ -16,7 +16,9 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -27,6 +29,7 @@
 import static org.mockito.Mockito.mock;
 
 import android.graphics.Rect;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -40,6 +43,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.window.flags.Flags;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -225,6 +229,53 @@
         });
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
+    public void testGetRoundedCornersRadius_letterboxBoundsMatchHeightInFreeform_notRounded() {
+        runTestScenario((robot) -> {
+            robot.conf().setLetterboxActivityCornersRadius(15);
+            robot.configureWindowState();
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            robot.activity().setTaskWindowingMode(WINDOWING_MODE_FREEFORM);
+            final Rect appBounds = new Rect(0, 0, 100, 500);
+            robot.configureWindowStateFrame(appBounds);
+            robot.activity().configureTaskAppBounds(appBounds);
+
+            robot.startLetterbox();
+
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 0);
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
+    public void testGetRoundedCornersRadius_letterboxBoundsNotMatchHeightInFreeform_rounded() {
+        runTestScenario((robot) -> {
+            robot.conf().setLetterboxActivityCornersRadius(15);
+            robot.configureWindowState();
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            robot.activity().setTaskWindowingMode(WINDOWING_MODE_FREEFORM);
+            robot.configureWindowStateFrame(new Rect(0, 0, 500, 200));
+            robot.activity().configureTaskAppBounds(new Rect(0, 0, 500, 500));
+
+            robot.startLetterbox();
+
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -265,6 +316,10 @@
             spyOn(getTransparentPolicy());
         }
 
+        void startLetterbox() {
+            getAppCompatLetterboxPolicy().start(mWindowState);
+        }
+
         void configureWindowStateWithTaskBar(boolean hasInsetsRoundedCorners) {
             configureWindowState(/* withTaskBar */ true, hasInsetsRoundedCorners);
         }
@@ -273,6 +328,10 @@
             configureWindowState(/* withTaskBar */ false, /* hasInsetsRoundedCorners */ false);
         }
 
+        void configureWindowStateFrame(@NonNull Rect frame) {
+            doReturn(frame).when(mWindowState).getFrame();
+        }
+
         void configureInsetsRoundedCorners(@NonNull RoundedCorners roundedCorners) {
             mInsetsState.setRoundedCorners(roundedCorners);
         }
@@ -290,6 +349,7 @@
             }
             mWindowState.mInvGlobalScale = 1f;
             final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+            attrs.type = TYPE_BASE_APPLICATION;
             doReturn(mInsetsState).when(mWindowState).getInsetsState();
             doReturn(attrs).when(mWindowState).getAttrs();
             doReturn(true).when(mWindowState).isDrawn();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index e6c3fb3..1e91bed 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.doReturn;
@@ -30,7 +28,6 @@
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.provider.Settings;
 import android.window.DesktopModeFlags;
 
 import androidx.test.filters.SmallTest;
@@ -74,14 +71,12 @@
         doReturn(mContext.getContentResolver()).when(mMockContext).getContentResolver();
         resetDesktopModeFlagsCache();
         resetEnforceDeviceRestriction();
-        resetFlagOverride();
     }
 
     @After
     public void tearDown() throws Exception {
         resetDesktopModeFlagsCache();
         resetEnforceDeviceRestriction();
-        resetFlagOverride();
     }
 
     @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
@@ -167,7 +162,8 @@
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @Test
-    public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() {
+    public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue()
+            throws Exception {
         doReturn(true).when(mMockResources).getBoolean(
                 eq(R.bool.config_isDesktopModeDevOptionSupported)
         );
@@ -246,13 +242,10 @@
         cachedToggleOverride.set(/* obj= */ null, /* value= */ null);
     }
 
-    private void resetFlagOverride() {
-        Settings.Global.putString(mContext.getContentResolver(),
-                DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null);
-    }
-
-    private void setFlagOverride(DesktopModeFlags.ToggleOverride override) {
-        Settings.Global.putInt(mContext.getContentResolver(),
-                DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting());
+    private void setFlagOverride(DesktopModeFlags.ToggleOverride override) throws Exception {
+        Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField(
+                "sCachedToggleOverride");
+        cachedToggleOverride.setAccessible(true);
+        cachedToggleOverride.set(/* obj= */ null, /* value= */ override);
     }
 }
\ No newline at end of file
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 82435b2..d5b9751 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -164,6 +164,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
 
 /**
  * Tests for the {@link DisplayContent} class.
@@ -1799,8 +1800,7 @@
         final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
         app.setVisible(false);
         app.setState(ActivityRecord.State.RESUMED, "test");
-        mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_OPEN);
-        mDisplayContent.mOpeningApps.add(app);
+        requestTransition(app, WindowManager.TRANSIT_OPEN);
         final int newOrientation = getRotatedOrientation(mDisplayContent);
         app.setRequestedOrientation(newOrientation);
 
@@ -2674,16 +2674,67 @@
     public void testKeyguardGoingAwayWhileAodShown() {
         mDisplayContent.getDisplayPolicy().setAwake(true);
 
-        final WindowState appWin = newWindowBuilder("appWin", TYPE_APPLICATION).setDisplay(
-                mDisplayContent).build();
-        final ActivityRecord activity = appWin.mActivityRecord;
+        final KeyguardController keyguard = mAtm.mKeyguardController;
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final int displayId = mDisplayContent.getDisplayId();
 
-        mAtm.mKeyguardController.setKeyguardShown(appWin.getDisplayId(), true /* keyguardShowing */,
-                true /* aodShowing */);
-        assertFalse(activity.isVisibleRequested());
+        final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
+        final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
+        final BooleanSupplier appVisible = activity::isVisibleRequested;
 
-        mAtm.mKeyguardController.keyguardGoingAway(appWin.getDisplayId(), 0 /* flags */);
-        assertTrue(activity.isVisibleRequested());
+        // Begin locked and in AOD
+        keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
+        assertFalse(keyguardGoingAway.getAsBoolean());
+        assertFalse(appVisible.getAsBoolean());
+
+        // Start unlocking from AOD.
+        keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
+        assertTrue(keyguardGoingAway.getAsBoolean());
+        assertTrue(appVisible.getAsBoolean());
+
+        // Clear AOD. This does *not* clear the going-away status.
+        keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+        assertTrue(keyguardGoingAway.getAsBoolean());
+        assertTrue(appVisible.getAsBoolean());
+
+        // Finish unlock
+        keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
+        assertFalse(keyguardGoingAway.getAsBoolean());
+        assertTrue(appVisible.getAsBoolean());
+    }
+
+    @Test
+    public void testKeyguardGoingAwayCanceledWhileAodShown() {
+        mDisplayContent.getDisplayPolicy().setAwake(true);
+
+        final KeyguardController keyguard = mAtm.mKeyguardController;
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final int displayId = mDisplayContent.getDisplayId();
+
+        final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
+        final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
+        final BooleanSupplier appVisible = activity::isVisibleRequested;
+
+        // Begin locked and in AOD
+        keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
+        assertFalse(keyguardGoingAway.getAsBoolean());
+        assertFalse(appVisible.getAsBoolean());
+
+        // Start unlocking from AOD.
+        keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
+        assertTrue(keyguardGoingAway.getAsBoolean());
+        assertTrue(appVisible.getAsBoolean());
+
+        // Clear AOD. This does *not* clear the going-away status.
+        keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+        assertTrue(keyguardGoingAway.getAsBoolean());
+        assertTrue(appVisible.getAsBoolean());
+
+        // Same API call a second time cancels the unlock, because AOD isn't changing.
+        keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+        assertTrue(keyguardShowing.getAsBoolean());
+        assertFalse(keyguardGoingAway.getAsBoolean());
+        assertFalse(appVisible.getAsBoolean());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 7ab55bf..cc2a76d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -189,11 +189,12 @@
         doReturn(true).when(mTaskFragment).isVisible();
         doReturn(true).when(mTaskFragment).isVisibleRequested();
 
+        spyOn(mTaskFragment.mTransitionController);
         clearInvocations(mTransaction);
         mTaskFragment.setBounds(endBounds);
 
         // No change transition, but update the organized surface position.
-        verify(mTaskFragment, never()).initializeChangeTransition(any(), any());
+        verify(mTaskFragment.mTransitionController, never()).collectVisibleChange(any());
         verify(mTransaction).setPosition(mLeash, endBounds.left, endBounds.top);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index edffab8..cee98fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -409,17 +409,6 @@
     }
 
     @Test
-    public void testIsAnimating_TransitionFlag() {
-        final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
-        final TestWindowContainer root = builder.setLayer(0).build();
-        final TestWindowContainer child1 = root.addChildWindow(
-                builder.setWaitForTransitionStart(true));
-
-        assertFalse(root.isAnimating(TRANSITION));
-        assertTrue(child1.isAnimating(TRANSITION));
-    }
-
-    @Test
     public void testIsAnimating_ParentsFlag() {
         final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
         final TestWindowContainer root = builder.setLayer(0).build();
@@ -1655,7 +1644,7 @@
         };
 
         TestWindowContainer(WindowManagerService wm, int layer, boolean isAnimating,
-                boolean isVisible, boolean waitTransitStart, Integer orientation, WindowState ws) {
+                boolean isVisible, Integer orientation, WindowState ws) {
             super(wm);
 
             mLayer = layer;
@@ -1663,7 +1652,6 @@
             mIsVisible = isVisible;
             mFillsParent = true;
             mOrientation = orientation;
-            mWaitForTransitStart = waitTransitStart;
             mWindowState = ws;
             spyOn(mSurfaceAnimator);
             doReturn(mIsAnimating).when(mSurfaceAnimator).isAnimating();
@@ -1729,11 +1717,6 @@
         }
 
         @Override
-        boolean isWaitingForTransitionStart() {
-            return mWaitForTransitStart;
-        }
-
-        @Override
         WindowState asWindowState() {
             return mWindowState;
         }
@@ -1744,7 +1727,6 @@
         private int mLayer;
         private boolean mIsAnimating;
         private boolean mIsVisible;
-        private boolean mIsWaitTransitStart;
         private Integer mOrientation;
         private WindowState mWindowState;
 
@@ -1782,14 +1764,9 @@
             return this;
         }
 
-        TestWindowContainerBuilder setWaitForTransitionStart(boolean waitTransitStart) {
-            mIsWaitTransitStart = waitTransitStart;
-            return this;
-        }
-
         TestWindowContainer build() {
             return new TestWindowContainer(mWm, mLayer, mIsAnimating, mIsVisible,
-                    mIsWaitTransitStart, mOrientation, mWindowState);
+                    mOrientation, mWindowState);
         }
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 1dfb20a..d228970 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
 import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
@@ -33,9 +34,11 @@
 import static com.android.server.wm.ActivityRecord.State.STARTED;
 import static com.android.server.wm.ActivityRecord.State.STOPPED;
 import static com.android.server.wm.ActivityRecord.State.STOPPING;
+import static com.android.server.wm.ConfigurationContainer.applySizeOverrideIfNeeded;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -58,6 +61,7 @@
 import android.graphics.Rect;
 import android.os.LocaleList;
 import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 
 import org.junit.Before;
@@ -453,6 +457,56 @@
         assertEquals(topDisplayArea, mWpc.getTopActivityDisplayArea());
     }
 
+    @Test
+    @EnableFlags(com.android.window.flags.Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION)
+    public void testOverrideConfigurationApplied() {
+        final DisplayContent displayContent = new TestDisplayContent.Builder(mAtm, 1000, 1500)
+                .setSystemDecorations(true).setDensityDpi(160).build();
+        final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
+        // Setup the decor insets info.
+        final DisplayPolicy.DecorInsets.Info decorInsetsInfo = new DisplayPolicy.DecorInsets.Info();
+        final Rect emptyRect = new Rect();
+        decorInsetsInfo.mNonDecorInsets.set(emptyRect);
+        decorInsetsInfo.mConfigInsets.set(emptyRect);
+        decorInsetsInfo.mOverrideConfigInsets.set(new Rect(0, 100, 0, 200));
+        decorInsetsInfo.mOverrideNonDecorInsets.set(new Rect(0, 0, 0, 200));
+        decorInsetsInfo.mNonDecorFrame.set(new Rect(0, 0, 1000, 1500));
+        decorInsetsInfo.mConfigFrame.set(new Rect(0, 0, 1000, 1500));
+        decorInsetsInfo.mOverrideConfigFrame.set(new Rect(0, 100, 1000, 1300));
+        decorInsetsInfo.mOverrideNonDecorFrame.set(new Rect(0, 0, 1000, 1300));
+        doReturn(decorInsetsInfo).when(displayPolicy)
+                .getDecorInsetsInfo(anyInt(), anyInt(), anyInt());
+
+        final Configuration newParentConfig = displayContent.getConfiguration();
+        final Configuration resolvedConfig = new Configuration();
+
+        // Mock the app info to not enforce the decoupled configuration to apply the override.
+        final ApplicationInfo appInfo = mock(ApplicationInfo.class);
+        doReturn(false).when(appInfo)
+                .isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED);
+        doReturn(false).when(appInfo)
+                .isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+
+        // No value should be set before override.
+        assertNull(resolvedConfig.windowConfiguration.getAppBounds());
+        applySizeOverrideIfNeeded(
+                displayContent,
+                appInfo,
+                newParentConfig,
+                resolvedConfig,
+                false /* optsOutEdgeToEdge */,
+                false /* hasFixedRotationTransform */,
+                false /* hasCompatDisplayInsets */,
+                null /* task */);
+
+        // Assert the override config insets are applied.
+        // Status bars, and all non-decor insets should be deducted for the config screen size.
+        assertEquals(1200, resolvedConfig.screenHeightDp);
+        // Only the non-decor insets should be deducted for the app bounds.
+        assertNotNull(resolvedConfig.windowConfiguration.getAppBounds());
+        assertEquals(1300, resolvedConfig.windowConfiguration.getAppBounds().height());
+    }
+
     private TestDisplayContent createTestDisplayContentInContainer() {
         return new TestDisplayContent.Builder(mAtm, 1000, 1500).build();
     }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0b3d720..1a932859 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -10207,6 +10207,17 @@
             "carrier_supported_satellite_notification_hysteresis_sec_int";
 
     /**
+     * Satellite notification display restriction reset time in seconds.
+     *
+     * The device shows a notification when it connects to a satellite.  If the user interacts
+     * with the notification, it won't be shown again immediately.  Instead, the notification
+     * will only reappear after below key mentioned amount of time has passed.
+     */
+    @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+    public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT =
+            "satellite_connected_notification_throttle_millis_int";
+
+    /**
      * An integer key holds the timeout duration in seconds used to determine whether to exit
      * carrier-roaming NB-IOT satellite mode.
      *
@@ -11428,6 +11439,10 @@
         sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT,
                 SatelliteManager.EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911);
         sDefaults.putInt(KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT, 180);
+        if (Flags.starlinkDataBugfix()) {
+            sDefaults.putLong(KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT,
+                    TimeUnit.DAYS.toMillis(7));
+        }
         sDefaults.putInt(KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30);
         sDefaults.putInt(KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT, 180);
         sDefaults.putInt(KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT, 600);
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 504605d..41569de 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -7080,7 +7080,8 @@
      */
     @Deprecated
     public boolean isVoiceCapable() {
-        return hasCapability(PackageManager.FEATURE_TELEPHONY_CALLING,
+        if (mContext == null) return true;
+        return mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_voice_capable);
     }
 
@@ -7104,7 +7105,8 @@
      * @see SubscriptionInfo#getServiceCapabilities()
      */
     public boolean isDeviceVoiceCapable() {
-        return isVoiceCapable();
+        return hasCapability(PackageManager.FEATURE_TELEPHONY_CALLING,
+                com.android.internal.R.bool.config_voice_capable);
     }
 
     /**
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index b7b209b..100690d 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -821,6 +821,25 @@
             "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT";
 
     /**
+     * A boolean value indicating whether application is optimized to utilize low bandwidth
+     * satellite data.
+     * The applications that are optimized for low bandwidth satellite data should set this
+     * property to {@code true} in the manifest to indicate to platform about the same.
+     * {@code
+     * <application>
+     *   <meta-data
+     *     android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
+     *     android:value="true"/>
+     * </application>
+     * }
+     * <p>
+     * When {@code true}, satellite data optimized network is available for applications.
+     */
+    @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+    public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED =
+            "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED";
+
+    /**
      * Registers a {@link SatelliteStateChangeListener} to receive callbacks when the satellite
      * state may have changed.
      *
@@ -3840,6 +3859,35 @@
         }
     }
 
+    /**
+     * Get list of application packages name that are optimized for low bandwidth satellite data.
+     *
+     * @return List of application packages name with data optimized network property.
+     *
+     * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED}
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+    public @NonNull List<String> getSatelliteDataOptimizedApps() {
+        List<String> appsNames = new ArrayList<>();
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                appsNames = telephony.getSatelliteDataOptimizedApps();
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("getSatelliteDataOptimizedApps() RemoteException:" + ex);
+            ex.rethrowAsRuntimeException();
+        }
+
+        return appsNames;
+    }
+
     @Nullable
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 08c0030..1c6652d 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3596,4 +3596,15 @@
      * @hide
      */
     int getCarrierIdFromIdentifier(in CarrierIdentifier carrierIdentifier);
+
+
+    /**
+     * Get list of applications that are optimized for low bandwidth satellite data.
+     *
+     * @return List of Application Name with data optimized network property.
+     * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED}
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+                      + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    List<String> getSatelliteDataOptimizedApps();
 }
diff --git a/tests/AppJankTest/res/values/strings.xml b/tests/AppJankTest/res/values/strings.xml
new file mode 100644
index 0000000..ab2d18f
--- /dev/null
+++ b/tests/AppJankTest/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+    <string name="continue_test">Continue Test</string>
+</resources>
\ No newline at end of file
diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
index fe9f636..3498974 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
@@ -209,7 +209,8 @@
                 JankTrackerActivity.class);
         resumeJankTracker.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
         mEmptyActivity.startActivity(resumeJankTracker);
-        mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS);
+        mDevice.wait(Until.findObject(By.text(mEmptyActivity.getString(R.string.continue_test))),
+                WAIT_FOR_TIMEOUT_MS);
 
         assertTrue(jankTracker.shouldTrack());
     }
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
index 80ab6ad3..6867582 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
@@ -18,15 +18,41 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.widget.EditText;
 
 
 public class JankTrackerActivity extends Activity {
 
+    private static final int CONTINUE_TEST_DELAY_MS = 4000;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.jank_tracker_activity_layout);
     }
+
+    /**
+     * In IntegrationTests#jankTrackingResumed_whenActivityBecomesVisibleAgain this activity is
+     * placed into the background and then resumed via an intent. The test waits until the
+     * `continue_test` string is visible on the screen before validating that Jank tracking has
+     * resumed.
+     *
+     * <p>The 4 second delay allows JankTracker to re-register its callbacks and start receiving
+     * JankData before the test proceeds.
+     */
+    @Override
+    protected void onResume() {
+        super.onResume();
+        getActivityThread().getHandler().postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                EditText editTextView = findViewById(R.id.edit_text);
+                if (editTextView != null) {
+                    editTextView.setText(R.string.continue_test);
+                }
+            }
+        }, CONTINUE_TEST_DELAY_MS);
+    }
 }
 
 
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
index c38517a..586bb76 100644
--- a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
@@ -277,6 +277,72 @@
         }
     }
 
+    @Test
+    public void checkRevocationStatus_allCertificatesRecentlyChecked_doesNotFetchRemoteCrl()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+        // indirectly verifies the remote list is not fetched by simulating a remote revocation
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+
+        // no exception
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+    }
+
+    @Test
+    public void checkRevocationStatus_allCertificatesBarelyRecentlyChecked_doesNotFetchRemoteCrl()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
+        LocalDateTime barelyRecently =
+                LocalDateTime.now()
+                        .minusHours(
+                                CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK - 1);
+        for (X509Certificate certificate : mCertificates1) {
+            lastCheckedDates.put(getSerialNumber(certificate), barelyRecently);
+        }
+        mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+
+        // Indirectly verify the remote CRL is not checked by checking there is no exception despite
+        // a certificate being revoked. This test differs from the next only in the lastCheckedDate,
+        // one before the NUM_HOURS_BEFORE_NEXT_CHECK cutoff and one after
+        mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+    }
+
+    @Test
+    public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException()
+            throws Exception {
+        copyFromAssetToFile(
+                REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+        mCertificateRevocationStatusManager =
+                new CertificateRevocationStatusManager(
+                        mContext, mRevocationListUrl, mRevocationStatusFile, false);
+        Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
+        // To save network use, we do not check the remote CRL if all the certificates are recently
+        // checked, so we set the lastCheckDate to some time not recent.
+        LocalDateTime notRecently =
+                LocalDateTime.now()
+                        .minusHours(
+                                CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK + 1);
+        for (X509Certificate certificate : mCertificates1) {
+            lastCheckedDates.put(getSerialNumber(certificate), notRecently);
+        }
+        mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+
+        assertThrows(
+                CertPathValidatorException.class,
+                () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+    }
+
     private List<X509Certificate> getCertificateChain(String fileName) throws Exception {
         Collection<? extends Certificate> certificates =
                 mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName));
diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
index 8ac3433..4c16a75 100644
--- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
@@ -48,8 +48,6 @@
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.os.test.TestLooper;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
@@ -305,90 +303,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
-    public void testBootLoopWithRescuePartyAndRollbackObserver() throws Exception {
-        PackageWatchdog watchdog = createWatchdog();
-        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
-        RollbackPackageHealthObserver rollbackObserver =
-                setUpRollbackPackageHealthObserver(watchdog);
-
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(1);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(1);
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
-            watchdog.noteBoot();
-        }
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(1);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(1);
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(2);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(3);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2);
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(3);
-        verify(rollbackObserver).onExecuteBootLoopMitigation(1);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2);
-        // Update the list of available rollbacks after executing bootloop mitigation once
-        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
-                ROLLBACK_INFO_MANUAL));
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(3);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(4);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2);
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(4);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(5);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2);
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(5);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(6);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2);
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(6);
-        verify(rollbackObserver).onExecuteBootLoopMitigation(2);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3);
-        // Update the list of available rollbacks after executing bootloop mitigation
-        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
-
-        watchdog.noteBoot();
-
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(6);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(7);
-        verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3);
-
-        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
-        Mockito.reset(rescuePartyObserver);
-
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
-            watchdog.noteBoot();
-        }
-        mTestLooper.dispatchAll();
-        verify(rescuePartyObserver).onExecuteBootLoopMitigation(1);
-        verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2);
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testBootLoopWithRescuePartyAndRollbackObserverNoFlags() throws Exception {
         PackageWatchdog watchdog = createWatchdog();
         RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
@@ -443,80 +357,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
-    public void testCrashLoopWithRescuePartyAndRollbackObserver() throws Exception {
-        PackageWatchdog watchdog = createWatchdog();
-        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
-        RollbackPackageHealthObserver rollbackObserver =
-                setUpRollbackPackageHealthObserver(watchdog);
-        VersionedPackage versionedPackageA = new VersionedPackage(APP_A, VERSION_CODE);
-
-        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
-            ApplicationInfo info = new ApplicationInfo();
-            info.flags |= ApplicationInfo.FLAG_PERSISTENT
-                    | ApplicationInfo.FLAG_SYSTEM;
-            return info;
-        });
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: SCOPED_DEVICE_CONFIG_RESET
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: ALL_DEVICE_CONFIG_RESET
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: WARM_REBOOT
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: Low impact rollback
-        verify(rollbackObserver).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-
-        // update available rollbacks to mock rollbacks being applied after the call to
-        // rollbackObserver.onExecuteHealthCheckMitigation
-        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
-                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD reached. No more mitigations applied
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testCrashLoopWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset()
             throws Exception {
         PackageWatchdog watchdog = createWatchdog();
@@ -569,123 +409,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
-    public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserver() throws Exception {
-        PackageWatchdog watchdog = createWatchdog();
-        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
-        RollbackPackageHealthObserver rollbackObserver =
-                setUpRollbackPackageHealthObserver(watchdog);
-        String systemUi = "com.android.systemui";
-        VersionedPackage versionedPackageUi = new VersionedPackage(
-                systemUi, VERSION_CODE);
-        RollbackInfo rollbackInfoUi = getRollbackInfo(systemUi, VERSION_CODE, 1,
-                PackageManager.ROLLBACK_USER_IMPACT_LOW);
-        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW,
-                ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL, rollbackInfoUi));
-
-        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
-            ApplicationInfo info = new ApplicationInfo();
-            info.flags |= ApplicationInfo.FLAG_PERSISTENT
-                    | ApplicationInfo.FLAG_SYSTEM;
-            return info;
-        });
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: SCOPED_DEVICE_CONFIG_RESET
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: ALL_DEVICE_CONFIG_RESET
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: WARM_REBOOT
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: Low impact rollback
-        verify(rollbackObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-
-        // update available rollbacks to mock rollbacks being applied after the call to
-        // rollbackObserver.onExecuteHealthCheckMitigation
-        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
-                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: RESET_SETTINGS_UNTRUSTED_DEFAULTS
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 5);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: RESET_SETTINGS_UNTRUSTED_CHANGES
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 5);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 6);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: RESET_SETTINGS_TRUSTED_DEFAULTS
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 6);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 7);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-
-        raiseFatalFailureAndDispatch(watchdog,
-                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
-
-        // Mitigation: Factory reset. High impact rollbacks are performed only for boot loops.
-        verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 7);
-        verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 8);
-        verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi,
-                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
     public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset()
             throws Exception {
         PackageWatchdog watchdog = createWatchdog();
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 83d22d9..4d379e4 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -18,18 +18,24 @@
 
 import static org.junit.Assert.assertTrue;
 
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.SystemClock;
+import android.os.TestLooperManager;
 import android.util.Log;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayDeque;
+import java.util.Queue;
 import java.util.concurrent.Executor;
 
 /**
@@ -44,7 +50,9 @@
  *     The Robolectric class also allows advancing time.
  */
 public class TestLooper {
-    protected final Looper mLooper;
+    private final Looper mLooper;
+    private final TestLooperManager mTestLooperManager;
+    private final Clock mClock;
 
     private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
     private static final Field THREAD_LOCAL_LOOPER_FIELD;
@@ -54,24 +62,46 @@
     private static final Method MESSAGE_MARK_IN_USE_METHOD;
     private static final String TAG = "TestLooper";
 
-    private final Clock mClock;
-
     private AutoDispatchThread mAutoDispatchThread;
 
+    /**
+     * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
+     */
+    private static boolean isAtLeastBaklava() {
+        Method[] methods = TestLooperManager.class.getMethods();
+        for (Method method : methods) {
+            if (method.getName().equals("peekWhen")) {
+                return true;
+            }
+        }
+        return false;
+        // TODO(shayba): delete the above, uncomment the below.
+        // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
+        // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+    }
+
     static {
         try {
             LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
             LOOPER_CONSTRUCTOR.setAccessible(true);
             THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
             THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
-            MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
-            MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
-            MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
-            MESSAGE_NEXT_FIELD.setAccessible(true);
-            MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
-            MESSAGE_WHEN_FIELD.setAccessible(true);
-            MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
-            MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+
+            if (isAtLeastBaklava()) {
+                MESSAGE_QUEUE_MESSAGES_FIELD = null;
+                MESSAGE_NEXT_FIELD = null;
+                MESSAGE_WHEN_FIELD = null;
+                MESSAGE_MARK_IN_USE_METHOD = null;
+            } else {
+                MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+                MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+                MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+                MESSAGE_NEXT_FIELD.setAccessible(true);
+                MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+                MESSAGE_WHEN_FIELD.setAccessible(true);
+                MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+                MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+            }
         } catch (NoSuchFieldException | NoSuchMethodException e) {
             throw new RuntimeException("Failed to initialize TestLooper", e);
         }
@@ -106,6 +136,13 @@
             throw new RuntimeException("Reflection error constructing or accessing looper", e);
         }
 
+        if (isAtLeastBaklava()) {
+            mTestLooperManager =
+                InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper);
+        } else {
+            mTestLooperManager = null;
+        }
+
         mClock = clock;
     }
 
@@ -117,19 +154,61 @@
         return new HandlerExecutor(new Handler(getLooper()));
     }
 
-    private Message getMessageLinkedList() {
+    private Message getMessageLinkedListLegacy() {
         try {
             MessageQueue queue = mLooper.getQueue();
             return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
         } catch (IllegalAccessException e) {
             throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
-                    e);
+                e);
         }
     }
 
     public void moveTimeForward(long milliSeconds) {
+        if (isAtLeastBaklava()) {
+            moveTimeForwardBaklava(milliSeconds);
+        } else {
+            moveTimeForwardLegacy(milliSeconds);
+        }
+    }
+
+    private void moveTimeForwardBaklava(long milliSeconds) {
+        // Drain all Messages from the queue.
+        Queue<Message> messages = new ArrayDeque<>();
+        while (true) {
+            Message message = mTestLooperManager.poll();
+            if (message == null) {
+                break;
+            }
+            messages.add(message);
+        }
+
+        // Repost all Messages back to the queue with a new time.
+        while (true) {
+            Message message = messages.poll();
+            if (message == null) {
+                break;
+            }
+
+            // Ugly trick to reset the Message's "in use" flag.
+            // This is needed because the Message cannot be re-enqueued if it's
+            // marked in use.
+            message.copyFrom(message);
+
+            // Adjust the Message's delivery time.
+            long newWhen = message.getWhen() - milliSeconds;
+            if (newWhen < 0) {
+                newWhen = 0;
+            }
+
+            // Send the Message back to its Handler to be re-enqueued.
+            message.getTarget().sendMessageAtTime(message, newWhen);
+        }
+    }
+
+    private void moveTimeForwardLegacy(long milliSeconds) {
         try {
-            Message msg = getMessageLinkedList();
+            Message msg = getMessageLinkedListLegacy();
             while (msg != null) {
                 long updatedWhen = msg.getWhen() - milliSeconds;
                 if (updatedWhen < 0) {
@@ -147,12 +226,12 @@
         return mClock.uptimeMillis();
     }
 
-    private Message messageQueueNext() {
+    private Message messageQueueNextLegacy() {
         try {
             long now = currentTime();
 
             Message prevMsg = null;
-            Message msg = getMessageLinkedList();
+            Message msg = getMessageLinkedListLegacy();
             if (msg != null && msg.getTarget() == null) {
                 // Stalled by a barrier. Find the next asynchronous message in
                 // the queue.
@@ -185,18 +264,46 @@
     /**
      * @return true if there are pending messages in the message queue
      */
-    public synchronized boolean isIdle() {
-        Message messageList = getMessageLinkedList();
+    public boolean isIdle() {
+        if (isAtLeastBaklava()) {
+            return isIdleBaklava();
+        } else {
+            return isIdleLegacy();
+        }
+    }
 
+    private boolean isIdleBaklava() {
+        Long when = mTestLooperManager.peekWhen();
+        return when != null && currentTime() >= when;
+    }
+
+    private synchronized boolean isIdleLegacy() {
+        Message messageList = getMessageLinkedListLegacy();
         return messageList != null && currentTime() >= messageList.getWhen();
     }
 
     /**
      * @return the next message in the Looper's message queue or null if there is none
      */
-    public synchronized Message nextMessage() {
+    public Message nextMessage() {
+        if (isAtLeastBaklava()) {
+            return nextMessageBaklava();
+        } else {
+            return nextMessageLegacy();
+        }
+    }
+
+    private Message nextMessageBaklava() {
         if (isIdle()) {
-            return messageQueueNext();
+            return mTestLooperManager.poll();
+        } else {
+            return null;
+        }
+    }
+
+    private synchronized Message nextMessageLegacy() {
+        if (isIdle()) {
+            return messageQueueNextLegacy();
         } else {
             return null;
         }
@@ -206,9 +313,26 @@
      * Dispatch the next message in the queue
      * Asserts that there is a message in the queue
      */
-    public synchronized void dispatchNext() {
+    public void dispatchNext() {
+        if (isAtLeastBaklava()) {
+            dispatchNextBaklava();
+        } else {
+            dispatchNextLegacy();
+        }
+    }
+
+    private void dispatchNextBaklava() {
         assertTrue(isIdle());
-        Message msg = messageQueueNext();
+        Message msg = mTestLooperManager.poll();
+        if (msg == null) {
+            return;
+        }
+        msg.getTarget().dispatchMessage(msg);
+    }
+
+    private synchronized void dispatchNextLegacy() {
+        assertTrue(isIdle());
+        Message msg = messageQueueNextLegacy();
         if (msg == null) {
             return;
         }
diff --git a/tests/vcn/Android.bp b/tests/vcn/Android.bp
index 51a300b..661ed07 100644
--- a/tests/vcn/Android.bp
+++ b/tests/vcn/Android.bp
@@ -16,13 +16,19 @@
     name: "FrameworksVcnTests",
     // For access hidden connectivity methods in tests
     defaults: ["framework-connectivity-test-defaults"],
+
+    // TODO: b/374174952 Use 36 after Android B finalization
+    min_sdk_version: "35",
+
     srcs: [
         "java/**/*.java",
         "java/**/*.kt",
     ],
     platform_apis: true,
-    test_suites: ["device-tests"],
-    certificate: "platform",
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
     static_libs: [
         "android.net.vcn.flags-aconfig-java-export",
         "androidx.test.rules",
diff --git a/tests/vcn/AndroidManifest.xml b/tests/vcn/AndroidManifest.xml
index a8f657c..08effbd 100644
--- a/tests/vcn/AndroidManifest.xml
+++ b/tests/vcn/AndroidManifest.xml
@@ -16,8 +16,9 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.frameworks.tests.vcn">
-    <uses-sdk android:minSdkVersion="33"
-        android:targetSdkVersion="33"/>
+    <!-- TODO: b/374174952 Use 36 after Android B finalization -->
+    <uses-sdk android:minSdkVersion="35" android:targetSdkVersion="35" />
+
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/vcn/AndroidTest.xml b/tests/vcn/AndroidTest.xml
index dc521fd..9c8362f 100644
--- a/tests/vcn/AndroidTest.xml
+++ b/tests/vcn/AndroidTest.xml
@@ -14,12 +14,20 @@
      limitations under the License.
 -->
 <configuration description="Runs VCN Tests.">
-    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="FrameworksVcnTests.apk" />
     </target_preparer>
 
     <option name="test-suite-tag" value="apct" />
     <option name="test-tag" value="FrameworksVcnTests" />
+
+    <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
+
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.frameworks.tests.vcn" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
index 1569613..0fa11ae 100644
--- a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
@@ -23,11 +23,24 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
 
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.HashSet;
 import java.util.Set;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnCellUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase {
     private static final Set<String> ALLOWED_PLMN_IDS = new HashSet<>();
     private static final Set<Integer> ALLOWED_CARRIER_IDS = new HashSet<>();
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
index 73a0a61..fa97de0 100644
--- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -29,11 +29,14 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.Build;
 import android.os.Parcel;
 import android.util.ArraySet;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -42,7 +45,10 @@
 import java.util.Collections;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnConfigTest {
     private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName();
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 59dc689..990cc74 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -34,10 +34,13 @@
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.net.vcn.persistablebundleutils.IkeSessionParamsUtilsTest;
 import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtilsTest;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,7 +52,10 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionConfigTest {
     // Public for use in VcnGatewayConnectionTest
diff --git a/tests/vcn/java/android/net/vcn/VcnManagerTest.java b/tests/vcn/java/android/net/vcn/VcnManagerTest.java
index 8461de6..1739fbc 100644
--- a/tests/vcn/java/android/net/vcn/VcnManagerTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnManagerTest.java
@@ -38,16 +38,28 @@
 import android.net.vcn.VcnManager.VcnStatusCallback;
 import android.net.vcn.VcnManager.VcnStatusCallbackBinder;
 import android.net.vcn.VcnManager.VcnUnderlyingNetworkPolicyListener;
+import android.os.Build;
 import android.os.ParcelUuid;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
 import java.net.UnknownHostException;
 import java.util.UUID;
 import java.util.concurrent.Executor;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnManagerTest {
     private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
     private static final String GATEWAY_CONNECTION_NAME = "gatewayConnectionName";
diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
index 7bc9970..52952eb 100644
--- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
@@ -30,12 +30,24 @@
 import android.net.NetworkCapabilities;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
+import android.os.Build;
 import android.os.Parcel;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.Arrays;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnTransportInfoTest {
     private static final int SUB_ID = 1;
     private static final int NETWORK_ID = 5;
diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
index a674425..c82d200 100644
--- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
@@ -22,9 +22,21 @@
 import static org.junit.Assert.assertNotEquals;
 
 import android.net.NetworkCapabilities;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnUnderlyingNetworkPolicyTest {
     private static final VcnUnderlyingNetworkPolicy DEFAULT_NETWORK_POLICY =
             new VcnUnderlyingNetworkPolicy(
diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
index 2110d6e..22361cc 100644
--- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
@@ -22,14 +22,20 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.TelephonyNetworkSpecifier;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnUnderlyingNetworkSpecifierTest {
     private static final int[] TEST_SUB_IDS = new int[] {1, 2, 3, 5};
diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
index 3ce6c8f..fb040d8 100644
--- a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
@@ -30,13 +30,25 @@
 import android.net.NetworkCapabilities;
 import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiInfo;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.Arrays;
 import java.util.Collections;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnUtilsTest {
     private static final int SUB_ID = 1;
 
diff --git a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
index 4063178..2c072e1 100644
--- a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
@@ -22,10 +22,23 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.Set;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnWifiUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase {
     private static final String SSID = "TestWifi";
 
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
index bc8e9d3..01e9ac2 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
@@ -21,11 +21,14 @@
 import static org.junit.Assert.assertEquals;
 
 import android.net.eap.EapSessionConfig;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,7 +38,10 @@
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class EapSessionConfigUtilsTest {
     private static final byte[] EAP_ID = "test@android.net".getBytes(StandardCharsets.US_ASCII);
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
index 4f3930f..821e5a6 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
@@ -25,10 +25,13 @@
 import android.net.ipsec.ike.IkeIpv6AddrIdentification;
 import android.net.ipsec.ike.IkeKeyIdIdentification;
 import android.net.ipsec.ike.IkeRfc822AddrIdentification;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,7 +42,10 @@
 
 import javax.security.auth.x500.X500Principal;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class IkeIdentificationUtilsTest {
     private static void verifyPersistableBundleEncodeDecodeIsLossless(IkeIdentification id) {
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
index 9f7d239..7200aee 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
@@ -29,14 +29,16 @@
 import android.net.eap.EapSessionConfig;
 import android.net.ipsec.ike.IkeFqdnIdentification;
 import android.net.ipsec.ike.IkeSessionParams;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.org.bouncycastle.util.io.pem.PemObject;
 import com.android.internal.org.bouncycastle.util.io.pem.PemReader;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,7 +54,10 @@
 import java.security.interfaces.RSAPrivateKey;
 import java.util.concurrent.TimeUnit;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class IkeSessionParamsUtilsTest {
     // Public for use in VcnGatewayConnectionConfigTest, EncryptedTunnelParamsUtilsTest
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
index 28cf38a..957e785d 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
@@ -20,17 +20,23 @@
 
 import android.net.InetAddresses;
 import android.net.ipsec.ike.IkeTrafficSelector;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.net.InetAddress;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class IkeTrafficSelectorUtilsTest {
     private static final int START_PORT = 16;
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
index 664044a..1e8f5ff 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
@@ -21,15 +21,21 @@
 import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.IkeSaProposal;
 import android.net.ipsec.ike.SaProposal;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class SaProposalUtilsTest {
     /** Package private so that IkeSessionParamsUtilsTest can use it */
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
index f9dc9eb..7d17724 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
@@ -20,14 +20,20 @@
 
 import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class TunnelConnectionParamsUtilsTest {
     // Public for use in VcnGatewayConnectionConfigTest
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
index e0b5f0e..3d7348a 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
@@ -25,10 +25,13 @@
 import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.IkeTrafficSelector;
 import android.net.ipsec.ike.TunnelModeChildSessionParams;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,7 +40,10 @@
 import java.net.Inet6Address;
 import java.util.concurrent.TimeUnit;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class TunnelModeChildSessionParamsUtilsTest {
     // Package private for use in EncryptedTunnelParamsUtilsTest
diff --git a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
index 47638b0..99c7aa7 100644
--- a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
@@ -33,9 +33,12 @@
 import static java.util.Collections.emptyList;
 
 import android.net.ipsec.ike.ChildSaProposal;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -43,7 +46,10 @@
 import java.util.Arrays;
 import java.util.List;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class MtuUtilsTest {
     private void verifyUnderlyingMtuZero(boolean isIpv4) {
diff --git a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
index c84e600..f7786af 100644
--- a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
@@ -21,10 +21,13 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,7 +38,10 @@
 import java.util.List;
 import java.util.Objects;
 
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class PersistableBundleUtilsTest {
     private static final String TEST_KEY = "testKey";
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 26a2a06..a97f9a8 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -79,6 +79,7 @@
 import android.net.vcn.VcnUnderlyingNetworkPolicy;
 import android.net.vcn.util.PersistableBundleUtils;
 import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
@@ -93,7 +94,6 @@
 import android.util.ArraySet;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.VcnManagementService.VcnCallback;
 import com.android.server.VcnManagementService.VcnStatusCallbackInfo;
@@ -101,6 +101,8 @@
 import com.android.server.vcn.Vcn;
 import com.android.server.vcn.VcnContext;
 import com.android.server.vcn.VcnNetworkProvider;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -117,8 +119,10 @@
 import java.util.Set;
 import java.util.UUID;
 
-/** Tests for {@link VcnManagementService}. */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnManagementServiceTest {
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 77f82f0..6276be2 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -54,6 +54,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.vcn.VcnManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
@@ -69,9 +70,10 @@
 import android.util.ArraySet;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.HandlerExecutor;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -87,8 +89,10 @@
 import java.util.Set;
 import java.util.UUID;
 
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class TelephonySubscriptionTrackerTest {
     private static final String PACKAGE_NAME =
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 74db6a5..6608dda 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -70,16 +70,18 @@
 import android.net.vcn.VcnManager.VcnErrorCode;
 import android.net.vcn.VcnTransportInfo;
 import android.net.vcn.util.MtuUtils;
+import android.os.Build;
 import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
 import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
 import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
 import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
 import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -94,8 +96,10 @@
 import java.util.List;
 import java.util.function.Consumer;
 
-/** Tests for VcnGatewayConnection.ConnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
     private static final int PARALLEL_SA_COUNT = 4;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
index 3c70759..f6123d2 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
@@ -26,17 +26,22 @@
 import static org.mockito.Mockito.verify;
 
 import android.net.ipsec.ike.IkeSessionParams;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
-/** Tests for VcnGatewayConnection.ConnectingState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectionTestBase {
     private VcnIkeSession mIkeSession;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
index f3eb82f..7cfaf5b 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
@@ -30,16 +30,21 @@
 import static org.mockito.Mockito.verify;
 
 import android.net.IpSecManager;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-/** Tests for VcnGatewayConnection.DisconnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnectionTestBase {
     @Before
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
index 78aefad..9132d83 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
@@ -23,15 +23,21 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.os.Build;
+
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-/** Tests for VcnGatewayConnection.DisconnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionDisconnectingStateTest extends VcnGatewayConnectionTestBase {
     @Before
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
index 6568cdd..d5ef4e0 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
@@ -27,15 +27,21 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.os.Build;
+
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-/** Tests for VcnGatewayConnection.RetryTimeoutState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionRetryTimeoutStateTest extends VcnGatewayConnectionTestBase {
     private long mFirstRetryInterval;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index b9fe76a..5283322 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -61,15 +61,17 @@
 import android.net.vcn.VcnManager;
 import android.net.vcn.VcnTransportInfo;
 import android.net.wifi.WifiInfo;
+import android.os.Build;
 import android.os.ParcelUuid;
 import android.os.Process;
 import android.telephony.SubscriptionInfo;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -87,8 +89,10 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase {
     private static final int TEST_UID = Process.myUid() + 1;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
index e9026e2..2b92428 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
@@ -29,12 +29,14 @@
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.NetworkRequest;
+import android.os.Build;
 import android.os.test.TestLooper;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,8 +46,10 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 @SmallTest
 public class VcnNetworkProviderTest {
     private static final int TEST_SCORE_UNSATISFIED = 0;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java
index 6d26968..bd4aeba 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java
@@ -49,20 +49,26 @@
 import android.net.vcn.VcnConfig;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.os.Build;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.server.VcnManagementService.VcnCallback;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
 import com.android.server.vcn.Vcn.VcnUserMobileDataStateListener;
 import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
@@ -73,6 +79,11 @@
 import java.util.Set;
 import java.util.UUID;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class VcnTest {
     private static final String PKG_NAME = VcnTest.class.getPackage().getName();
     private static final ParcelUuid TEST_SUB_GROUP = new ParcelUuid(new UUID(0, 0));
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
index c11b6bb..53a36d3 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -44,16 +44,22 @@
 import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.net.IpSecTransformState;
+import android.os.Build;
 import android.os.OutcomeReceiver;
 import android.os.PowerManager;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculationResult;
 import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator;
 import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper;
 import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -63,6 +69,11 @@
 import java.util.BitSet;
 import java.util.concurrent.TimeUnit;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
     private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName();
 
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index 4f34f9f..a9c637f 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -42,16 +42,28 @@
 import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
+import android.os.Build;
 import android.os.PersistableBundle;
 import android.util.ArraySet;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase {
     private UnderlyingNetworkRecord mWifiNetworkRecord;
     private UnderlyingNetworkRecord mCellNetworkRecord;
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index e540932..99c508c 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -58,6 +58,7 @@
 import android.net.vcn.VcnCellUnderlyingNetworkTemplateTest;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.Build;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
@@ -65,6 +66,8 @@
 import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 
+import androidx.test.filters.SmallTest;
+
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
 import com.android.server.vcn.VcnNetworkProvider;
@@ -73,9 +76,12 @@
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
 import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -89,6 +95,11 @@
 import java.util.Set;
 import java.util.UUID;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class UnderlyingNetworkControllerTest {
     private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
     private static final int INITIAL_SUB_ID_1 = 1;
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
index a315b069..27c1bc1 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -38,19 +38,30 @@
 
 import android.net.IpSecTransform;
 import android.net.vcn.VcnGatewayConnectionConfig;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies;
 import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
 
 import java.util.concurrent.TimeUnit;
 
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
 public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
     private static final int PENALTY_TIMEOUT_MIN = 10;
     private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN);
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index e24fe07..9ef8b7d 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -349,20 +349,22 @@
             value->value->Accept(&body_printer);
             printer->Undent();
           }
-          printer->Println("Flag disabled values:");
-          for (const auto& value : entry.flag_disabled_values) {
-            printer->Print("(");
-            printer->Print(value->config.to_string());
-            printer->Print(") ");
-            value->value->Accept(&headline_printer);
-            if (options.show_sources && !value->value->GetSource().path.empty()) {
-              printer->Print(" src=");
-              printer->Print(value->value->GetSource().to_string());
+          if (!entry.flag_disabled_values.empty()) {
+            printer->Println("Flag disabled values:");
+            for (const auto& value : entry.flag_disabled_values) {
+              printer->Print("(");
+              printer->Print(value->config.to_string());
+              printer->Print(") ");
+              value->value->Accept(&headline_printer);
+              if (options.show_sources && !value->value->GetSource().path.empty()) {
+                printer->Print(" src=");
+                printer->Print(value->value->GetSource().to_string());
+              }
+              printer->Println();
+              printer->Indent();
+              value->value->Accept(&body_printer);
+              printer->Undent();
             }
-            printer->Println();
-            printer->Indent();
-            value->value->Accept(&body_printer);
-            printer->Undent();
           }
           printer->Undent();
         }