Merge "Add functional tests for desktop mode scenario tests." into main
diff --git a/OWNERS b/OWNERS
index bde7ab2..096da29 100644
--- a/OWNERS
+++ b/OWNERS
@@ -14,6 +14,7 @@
 nandana@google.com #{LAST_RESORT_SUGGESTION}
 narayan@google.com #{LAST_RESORT_SUGGESTION}
 ogunwale@google.com #{LAST_RESORT_SUGGESTION}
+omakoto@google.com #{LAST_RESORT_SUGGESTION}
 roosa@google.com #{LAST_RESORT_SUGGESTION}
 smoreland@google.com #{LAST_RESORT_SUGGESTION}
 yamasani@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 9a178e5..18ee6f2 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -165,6 +165,7 @@
 import com.android.server.SystemServiceManager;
 import com.android.server.SystemTimeZone;
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.permission.PermissionManagerService;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -3763,8 +3764,10 @@
             }
             mNextAlarmClockForUser.put(userId, alarmClock);
             if (mStartUserBeforeScheduledAlarms) {
-                mUserWakeupStore.addUserWakeup(userId, convertToElapsed(
-                        mNextAlarmClockForUser.get(userId).getTriggerTime(), RTC));
+                if (shouldAddWakeupForUser(userId)) {
+                    mUserWakeupStore.addUserWakeup(userId, convertToElapsed(
+                            mNextAlarmClockForUser.get(userId).getTriggerTime(), RTC));
+                }
             }
         } else {
             if (DEBUG_ALARM_CLOCK) {
@@ -3784,6 +3787,23 @@
     }
 
     /**
+     * Checks whether the user is of type that needs to be started before the alarm.
+     */
+    @VisibleForTesting
+    boolean shouldAddWakeupForUser(@UserIdInt int userId) {
+        final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
+        if (umInternal.getUserInfo(userId) == null || umInternal.getUserInfo(userId).isGuest()) {
+            // Guest user should not be started in the background.
+            return false;
+        } else {
+            // SYSTEM user is always running, so no need to schedule wakeup for it.
+            // Profiles are excluded from the wakeup list because users can explicitly stop them and
+            // so starting them in the background would go against the user's intent.
+            return userId != UserHandle.USER_SYSTEM && umInternal.getUserInfo(userId).isFull();
+        }
+    }
+
+    /**
      * Updates NEXT_ALARM_FORMATTED and sends NEXT_ALARM_CLOCK_CHANGED_INTENT for all users
      * for which alarm clocks have changed since the last call to this.
      *
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index 93904a7..9fe197d 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.os.Environment;
 import android.os.SystemClock;
-import android.os.UserHandle;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -119,13 +118,10 @@
      * @param alarmTime time when alarm is expected to trigger.
      */
     public void addUserWakeup(int userId, long alarmTime) {
-        // SYSTEM user is always running, so no need to schedule wakeup for it.
-        if (userId != UserHandle.USER_SYSTEM) {
-            synchronized (mUserWakeupLock) {
-                mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
-            }
-            updateUserListFile();
+        synchronized (mUserWakeupLock) {
+            mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
         }
+        updateUserListFile();
     }
 
     /**
diff --git a/core/api/current.txt b/core/api/current.txt
index c5a70df..354e26b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -43729,6 +43729,7 @@
     field public static final String KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY = "carrier_nr_availabilities_int_array";
     field public static final String KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL = "carrier_provisions_wifi_merged_networks_bool";
     field public static final String KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL = "carrier_rcs_provisioning_required_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT = "carrier_roaming_ntn_emergency_call_to_satellite_handover_type_int";
     field public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array";
     field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array";
     field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string";
@@ -43911,6 +43912,7 @@
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = "satellite_screen_off_inactivity_timeout_duration_sec_int";
     field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool";
     field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool";
     field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool";
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index e148b5c..df707d1 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -321,7 +321,7 @@
 package android.net.wifi {
 
   public final class WifiMigration {
-    method @FlaggedApi("android.net.wifi.flags.legacy_keystore_to_wifi_blobstore_migration") public static void migrateLegacyKeystoreToWifiBlobstore();
+    method @FlaggedApi("android.net.wifi.flags.legacy_keystore_to_wifi_blobstore_migration_read_only") public static void migrateLegacyKeystoreToWifiBlobstore();
   }
 
 }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fd0262e..bed1b43 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3465,6 +3465,7 @@
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName, int);
     method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
@@ -3489,8 +3490,10 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName, int);
     method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int, int);
     method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDisplayImePolicy(int, int);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 128fb62..92bca3c 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -22,11 +22,9 @@
         ":framework-nfc-non-updatable-sources",
         ":messagequeue-gen",
     ],
-    // Exactly one of the below will be added to srcs by messagequeue-gen
+    // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
     exclude_srcs: [
-        "android/os/LegacyMessageQueue/MessageQueue.java",
-        "android/os/ConcurrentMessageQueue/MessageQueue.java",
-        "android/os/SemiConcurrentMessageQueue/MessageQueue.java",
+        "android/os/*MessageQueue/**/*.java",
     ],
     visibility: ["//frameworks/base"],
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 07e4f22..d455853 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -232,6 +232,7 @@
 import com.android.internal.content.ReferrerIntent;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderInternal;
+import com.android.internal.os.DebugStore;
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.os.SafeZipPathValidatorCallback;
 import com.android.internal.os.SomeArgs;
@@ -358,6 +359,15 @@
     private static final long BINDER_CALLBACK_THROTTLE = 10_100L;
     private long mBinderCallbackLast = -1;
 
+    private static final boolean DEBUG_STORE_ENABLED =
+            com.android.internal.os.Flags.debugStoreEnabled();
+
+    /**
+    * Threshold for identifying long-running looper messages (in milliseconds).
+    * Calculated as 2 seconds multiplied by the hardware timeout multiplier.
+    */
+    private static final long LONG_MESSAGE_THRESHOLD_MS = 2000 * Build.HW_TIMEOUT_MULTIPLIER;
+
     /**
      * Denotes the sequence number of the process state change for which the main thread needs
      * to block until the network rules are updated for it.
@@ -2395,6 +2405,12 @@
         }
         public void handleMessage(Message msg) {
             if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
+            long debugStoreId = -1;
+            // By default, log all long messages when the debug store is enabled,
+            // unless this is overridden for certain message types, for which we have
+            // more granular debug store logging.
+            boolean shouldLogLongMessage = DEBUG_STORE_ENABLED;
+            final long messageStartUptimeMs = SystemClock.uptimeMillis();
             switch (msg.what) {
                 case BIND_APPLICATION:
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
@@ -2419,24 +2435,61 @@
                                     "broadcastReceiveComp");
                         }
                     }
-                    handleReceiver((ReceiverData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    ReceiverData receiverData = (ReceiverData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId =
+                                DebugStore.recordBroadcastHandleReceiver(receiverData.intent);
+                    }
+
+                    try {
+                        handleReceiver(receiverData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case CREATE_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 ("serviceCreate: " + String.valueOf(msg.obj)));
                     }
-                    handleCreateService((CreateServiceData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    CreateServiceData createServiceData = (CreateServiceData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId = DebugStore.recordServiceCreate(createServiceData.info);
+                    }
+
+                    try {
+                        handleCreateService(createServiceData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case BIND_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind: "
                                 + String.valueOf(msg.obj));
                     }
-                    handleBindService((BindServiceData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    BindServiceData bindData = (BindServiceData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId =
+                                DebugStore.recordServiceBind(bindData.rebind, bindData.intent);
+                    }
+                    try {
+                        handleBindService(bindData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case UNBIND_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
@@ -2452,8 +2505,21 @@
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 ("serviceStart: " + String.valueOf(msg.obj)));
                     }
-                    handleServiceArgs((ServiceArgsData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    ServiceArgsData serviceData = (ServiceArgsData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId = DebugStore.recordServiceOnStart(serviceData.startId,
+                                serviceData.flags, serviceData.args);
+                    }
+
+                    try {
+                        handleServiceArgs(serviceData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case STOP_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
@@ -2649,11 +2715,17 @@
                     handleFinishInstrumentationWithoutRestart();
                     break;
             }
+            long messageElapsedTimeMs = SystemClock.uptimeMillis() - messageStartUptimeMs;
             Object obj = msg.obj;
             if (obj instanceof SomeArgs) {
                 ((SomeArgs) obj).recycle();
             }
             if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
+            if (shouldLogLongMessage
+                    && messageElapsedTimeMs > LONG_MESSAGE_THRESHOLD_MS) {
+                DebugStore.recordLongLooperMessage(msg.what, msg.getTarget().getClass().getName(),
+                        messageElapsedTimeMs);
+            }
         }
     }
 
@@ -7066,9 +7138,6 @@
                     && level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
                 return;
             }
-            if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
-                PropertyInvalidatedCache.onTrimMemory();
-            }
 
             final ArrayList<ComponentCallbacks2> callbacks =
                     collectComponentCallbacks(true /* includeUiContexts */);
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 27f9f54..22804a2 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1617,18 +1617,4 @@
             Log.e(TAG, "Failed to dump PropertyInvalidatedCache instances");
         }
     }
-
-    /**
-     * Trim memory by clearing all the caches.
-     * @hide
-     */
-    public static void onTrimMemory() {
-        ArrayList<PropertyInvalidatedCache> activeCaches;
-        synchronized (sGlobalLock) {
-            activeCaches = getActiveCaches();
-        }
-        for (int i = 0; i < activeCaches.size(); i++) {
-            activeCaches.get(i).clear();
-        }
-    }
 }
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a7070b9..965e3c4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -11964,6 +11964,34 @@
     }
 
     /**
+     * Adds a user restriction on {@code targetUser}, specified by the {@code key}.
+     *
+     * <p>Called by a system service only, meaning that the caller's UID must be equal to
+     * {@link Process#SYSTEM_UID}.
+     *
+     * @param systemEntity  The service entity that adds the restriction. A user restriction set by
+     *                       a service entity can only be cleared by the same entity. This can be
+     *                       just the calling package name, or any string of the caller's choice
+     *                       can be used.
+     * @param key  The key of the restriction.
+     * @param targetUser  The user to add the restriction on.
+     * @throws SecurityException if the caller is not a system service
+     *
+     * @hide
+     */
+    public void addUserRestriction(@NonNull String systemEntity,
+            @NonNull @UserManager.UserRestrictionKey String key, @UserIdInt int targetUser) {
+        if (mService != null) {
+            try {
+                mService.setUserRestrictionForUser(
+                        systemEntity, key, /* enable= */ true, targetUser);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
      * Called by a profile owner, device owner or a holder of any permission that is associated with
      *  a user restriction to set a user restriction specified by the provided {@code key} globally
      *  on all users. To clear the restriction use {@link #clearUserRestriction}.
@@ -11971,7 +11999,7 @@
      * <p>For a given user, a restriction will be set if it was applied globally or locally by any
      * admin.
      *
-     * <p> The calling device admin must be a profile owner, device owner or or a holder of any
+     * <p> The calling device admin must be a profile owner, device owner or a holder of any
      * permission that is associated with a user restriction; if it is not, a security
      * exception will be thrown.
      *
@@ -12072,6 +12100,34 @@
     }
 
     /**
+     * Clears a user restriction from {@code targetUser}, specified by the {@code key}.
+     *
+     * <p>Called by a system service only, meaning that the caller's UID must be equal to
+     * {@link Process#SYSTEM_UID}.
+     *
+     * @param systemEntity  The system entity that clears the restriction. A user restriction
+     *                         set by a system entity can only be cleared by the same entity. This
+     *                         can be just the calling package name, or any string of the caller's
+     *                         choice can be used.
+     * @param key  The key of the restriction.
+     * @param targetUser  The user to clear the restriction from.
+     * @throws SecurityException if the caller is not a system service
+     *
+     * @hide
+     */
+    public void clearUserRestriction(@NonNull String systemEntity,
+            @NonNull @UserManager.UserRestrictionKey String key, @UserIdInt int targetUser) {
+        if (mService != null) {
+            try {
+                mService.setUserRestrictionForUser(
+                        systemEntity, key, /* enable= */ false, targetUser);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
      * Called by an admin to get user restrictions set by themselves with
      * {@link #addUserRestriction(ComponentName, String)}.
      * <p>
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 381f996..c393a9e 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -255,6 +255,7 @@
     ComponentName getRestrictionsProvider(int userHandle);
 
     void setUserRestriction(in ComponentName who, in String callerPackage, in String key, boolean enable, boolean parent);
+    void setUserRestrictionForUser(in String systemEntity, in String key, boolean enable, int targetUser);
     void setUserRestrictionGlobally(in String callerPackage, in String key);
     Bundle getUserRestrictions(in ComponentName who, in String callerPackage, boolean parent);
     Bundle getUserRestrictionsGlobally(in String callerPackage);
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index 1f19f81..933c336 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -132,7 +132,7 @@
      * Create a host view. Uses specified animations when pushing
      * {@link #updateAppWidget(RemoteViews)}.
      *
-     * @param animationIn Resource ID of in animation to use
+     * @param animationIn  Resource ID of in animation to use
      * @param animationOut Resource ID of out animation to use
      */
     @SuppressWarnings({"UnusedDeclaration"})
@@ -148,7 +148,7 @@
      * Pass the given handler to RemoteViews when updating this widget. Unless this
      * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
      * should be made.
-     * @param handler
+     *
      * @hide
      */
     public void setInteractionHandler(InteractionHandler handler) {
@@ -206,10 +206,10 @@
      * order for the AppWidgetHost to account for the automatic padding when computing the number
      * of cells to allocate to a particular widget.
      *
-     * @param context the current context
+     * @param context   the current context
      * @param component the component name of the widget
-     * @param padding Rect in which to place the output, if null, a new Rect will be allocated and
-     *                returned
+     * @param padding   Rect in which to place the output, if null, a new Rect will be allocated and
+     *                  returned
      * @return default padding for this widget, in pixels
      */
     public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
@@ -291,7 +291,7 @@
         }
         mDelayedRestoredInflationId = -1;
         mDelayedRestoredState = null;
-        try  {
+        try {
             super.dispatchRestoreInstanceState(state);
         } catch (Exception e) {
             Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
@@ -354,14 +354,14 @@
      * the framework will be accounted for automatically. This information gets embedded into the
      * AppWidget options and causes a callback to the AppWidgetProvider. In addition, the list of
      * sizes is explicitly set to an empty list.
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      *
      * @param newOptions The bundle of options, in addition to the size information,
-     *          can be null.
-     * @param minWidth The minimum width in dips that the widget will be displayed at.
-     * @param minHeight The maximum height in dips that the widget will be displayed at.
-     * @param maxWidth The maximum width in dips that the widget will be displayed at.
-     * @param maxHeight The maximum height in dips that the widget will be displayed at.
+     *                   can be null.
+     * @param minWidth   The minimum width in dips that the widget will be displayed at.
+     * @param minHeight  The maximum height in dips that the widget will be displayed at.
+     * @param maxWidth   The maximum width in dips that the widget will be displayed at.
+     * @param maxHeight  The maximum height in dips that the widget will be displayed at.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      * @deprecated use {@link AppWidgetHostView#updateAppWidgetSize(Bundle, List)} instead.
      */
     @Deprecated
@@ -378,12 +378,14 @@
      * This method will update the option bundle with the list of sizes and the min/max bounds for
      * width and height.
      *
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
-     *
      * @param newOptions The bundle of options, in addition to the size information.
-     * @param sizes Sizes, in dips, the widget may be displayed at without calling the provider
-     *              again. Typically, this will be size of the widget in landscape and portrait.
-     *              On some foldables, this might include the size on the outer and inner screens.
+     * @param sizes      Sizes, in dips, the widget may be displayed at without calling the
+     *                   provider
+     *                   again. Typically, this will be size of the widget in landscape and
+     *                   portrait.
+     *                   On some foldables, this might include the size on the outer and inner
+     *                   screens.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      */
     public void updateAppWidgetSize(@NonNull Bundle newOptions, @NonNull List<SizeF> sizes) {
         AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
@@ -470,9 +472,9 @@
     /**
      * Specify some extra information for the widget provider. Causes a callback to the
      * AppWidgetProvider.
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      *
      * @param options The bundle of options information.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      */
     public void updateAppWidgetOptions(Bundle options) {
         AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
@@ -507,6 +509,7 @@
     /**
      * Sets whether the widget is being displayed on a light/white background and use an
      * alternate UI if available.
+     *
      * @see RemoteViews#setLightBackgroundLayoutId(int)
      */
     public void setOnLightBackground(boolean onLightBackground) {
@@ -620,7 +623,7 @@
         if (content == null) {
             if (mViewMode == VIEW_MODE_ERROR) {
                 // We've already done this -- nothing to do.
-                return ;
+                return;
             }
             if (exception != null) {
                 Log.w(TAG, "Error inflating RemoteViews", exception);
@@ -733,7 +736,7 @@
             if (adapter instanceof BaseAdapter) {
                 BaseAdapter baseAdapter = (BaseAdapter) adapter;
                 baseAdapter.notifyDataSetChanged();
-            }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
+            } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
                 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
                 // connected to its associated service, and hence the adapter hasn't been set.
                 // In this case, we need to defer the notify call until it has been set.
@@ -745,6 +748,7 @@
     /**
      * Build a {@link Context} cloned into another package name, usually for the
      * purposes of reading remote resources.
+     *
      * @hide
      */
     protected Context getRemoteContextEnsuringCorrectCachedApkPath() {
@@ -760,7 +764,7 @@
             }
             return newContext;
         } catch (NameNotFoundException e) {
-            Log.e(TAG, "Package name " +  mInfo.providerInfo.packageName + " not found");
+            Log.e(TAG, "Package name " + mInfo.providerInfo.packageName + " not found");
             return mContext;
         } catch (NullPointerException e) {
             Log.e(TAG, "Error trying to create the remote context.", e);
@@ -774,7 +778,7 @@
      */
     protected void prepareView(View view) {
         // Take requested dimensions from child, but apply default gravity.
-        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
+        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams) view.getLayoutParams();
         if (requested == null) {
             requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                     LayoutParams.MATCH_PARENT);
@@ -839,7 +843,18 @@
         return defaultView;
     }
 
-    private void onDefaultViewClicked(View view) {
+    /**
+     * Handles interactions on the default view of the widget. By default does not use the
+     * {@link InteractionHandler} used by other interactions. However, this can be overridden
+     * in order to customize the click behavior.
+     *
+     * @hide
+     */
+    protected void onDefaultViewClicked(@NonNull View view) {
+        final AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+        if (manager != null) {
+            manager.noteAppWidgetTapped(mAppWidgetId);
+        }
         if (mInfo != null) {
             LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class);
             List<LauncherActivityInfo> activities = launcherApps.getActivityList(
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 0653839..a56bc02 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -112,6 +112,24 @@
     void removeActivityPolicyExemption(in ComponentName exemption);
 
     /**
+     * Specifies a policy for this virtual device on the given display.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void setDevicePolicyForDisplay(int displayId, int policyType, int devicePolicy);
+
+    /**
+     * Adds an exemption to the default activity launch policy on the given display.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void addActivityPolicyExemptionForDisplay(int displayId, in ComponentName exemption);
+
+    /**
+     * Removes an exemption to the default activity launch policy on the given display.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void removeActivityPolicyExemptionForDisplay(int displayId, in ComponentName exemption);
+
+    /**
      * Notifies that an audio session being started.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index d3fcfc6..d8899b2 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -17,7 +17,11 @@
 package android.companion.virtual;
 
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
@@ -282,6 +286,16 @@
 
     void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
+        switch (policyType) {
+            case POLICY_TYPE_RECENTS:
+            case POLICY_TYPE_CLIPBOARD:
+            case POLICY_TYPE_ACTIVITY:
+            case POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR:
+                break;
+            default:
+                throw new IllegalArgumentException("Device policy " + policyType
+                        + " cannot be changed at runtime. ");
+        }
         try {
             mVirtualDevice.setDevicePolicy(policyType, devicePolicy);
         } catch (RemoteException e) {
@@ -305,6 +319,42 @@
         }
     }
 
+    void setDevicePolicyForDisplay(int displayId,
+            @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
+            @VirtualDeviceParams.DevicePolicy int devicePolicy) {
+        switch (policyType) {
+            case POLICY_TYPE_RECENTS:
+            case POLICY_TYPE_ACTIVITY:
+                break;
+            default:
+                throw new IllegalArgumentException("Device policy " + policyType
+                        + " cannot be changed for a specific display. ");
+        }
+
+        try {
+            mVirtualDevice.setDevicePolicyForDisplay(displayId, policyType, devicePolicy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    void addActivityPolicyExemptionForDisplay(int displayId, @NonNull ComponentName componentName) {
+        try {
+            mVirtualDevice.addActivityPolicyExemptionForDisplay(displayId, componentName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    void removeActivityPolicyExemptionForDisplay(int displayId,
+            @NonNull ComponentName componentName) {
+        try {
+            mVirtualDevice.removeActivityPolicyExemptionForDisplay(displayId, componentName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
         try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 296ca33..42da7e9 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -739,6 +739,7 @@
          *
          * @param policyType the type of policy, i.e. which behavior to specify a policy for.
          * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior.
+         * @throws IllegalArgumentException if the policy cannot be changed at runtime.
          *
          * @see VirtualDeviceParams#POLICY_TYPE_RECENTS
          * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
@@ -797,6 +798,81 @@
         }
 
         /**
+         * Specifies a policy for this virtual device to be applied on the given virtual display.
+         * <p>
+         * Any policy specified for a particular display takes precedence over the policy specified
+         * for the device itself.
+         * </p>
+         *
+         * @param policyType the type of policy, i.e. which behavior to specify a policy for.
+         * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior.
+         * @param displayId the ID of the display, for which to apply the policy.
+         * @throws IllegalArgumentException if the specified policy cannot be changed per
+         *   display or if the specified display does not belong to the virtual device.
+         *
+         * @see #setDevicePolicy(int, int)
+         * @see VirtualDeviceParams#POLICY_TYPE_RECENTS
+         * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        public void setDevicePolicy(
+                @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
+                @VirtualDeviceParams.DevicePolicy int devicePolicy,
+                int displayId) {
+            mVirtualDeviceInternal.setDevicePolicyForDisplay(displayId, policyType, devicePolicy);
+        }
+
+        /**
+         * Specifies a component name to be exempt from the given display's activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
+         * then the specified component will be blocked from launching.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
+         * specified component will be allowed to launch.</p>
+         *
+         * <p>Note that changing the activity launch policy will clear current set of exempt
+         * components.</p>
+         * <p>Any change to the exemptions will only be applied for new activity launches.</p>
+         *
+         * @see #removeActivityPolicyExemption
+         * @see #setDevicePolicy
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        public void addActivityPolicyExemption(
+                @NonNull ComponentName componentName, int displayId) {
+            mVirtualDeviceInternal.addActivityPolicyExemptionForDisplay(
+                    displayId, Objects.requireNonNull(componentName));
+        }
+
+        /**
+         * Makes the specified component name adhere to the given display's activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}),
+         * then the specified component will be allowed to launch.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches
+         * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the
+         * specified component will be blocked from launching.</p>
+         *
+         * <p>Note that changing the activity launch policy will clear current set of exempt
+         * components.</p>
+         *
+         * @see #addActivityPolicyExemption
+         * @see #setDevicePolicy
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        public void removeActivityPolicyExemption(
+                @NonNull ComponentName componentName, int displayId) {
+            mVirtualDeviceInternal.removeActivityPolicyExemptionForDisplay(
+                    displayId, Objects.requireNonNull(componentName));
+        }
+
+        /**
          * Creates a virtual dpad.
          *
          * @param config the configurations of the virtual dpad.
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index f7f842f..c1fc51d 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,8 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
-            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA})
+            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
+            POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -177,6 +178,17 @@
     public @interface DynamicPolicyType {}
 
     /**
+     * Policy types that can be dynamically changed for a specific display.
+     *
+     * @see VirtualDeviceManager.VirtualDevice#setDevicePolicyForDisplay
+     * @hide
+     */
+    @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    public @interface DynamicDisplayPolicyType {}
+
+    /**
      * Tells the sensor framework how to handle sensor requests from contexts associated with this
      * virtual device, namely the sensors returned by
      * {@link android.hardware.SensorManager#getSensorList}:
@@ -229,6 +241,8 @@
      * @see VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption
      * @see VirtualDeviceManager.VirtualDevice#removeActivityPolicyExemption
      */
+    // TODO(b/333443509): Update the documentation of custom policy and link to the new policy
+    // POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR
     @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
     public static final int POLICY_TYPE_ACTIVITY = 3;
 
@@ -276,6 +290,7 @@
      *     experience on the virtual device.
      * </ul>
      */
+    // TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY
     @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
     public static final int POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR = 6;
 
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index b29b52d..91586b6 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -81,13 +81,6 @@
 }
 
 flag {
-  name: "stream_permissions"
-  namespace: "virtual_devices"
-  description: "Enable streaming permission dialogs to Virtual Devices"
-  bug: "291737919"
-}
-
-flag {
   name: "persistent_device_id_api"
   is_exported: true
   namespace: "virtual_devices"
@@ -96,13 +89,6 @@
 }
 
 flag {
-  name: "express_metrics"
-  namespace: "virtual_devices"
-  description: "Enable express metrics in VDM"
-  bug: "307297730"
-}
-
-flag {
   name: "interactive_screen_mirror"
   is_exported: true
   namespace: "virtual_devices"
@@ -119,17 +105,6 @@
 }
 
 flag {
-  name: "intercept_intents_before_applying_policy"
-  is_exported: true
-  namespace: "virtual_devices"
-  description: "Apply intent interception before applying activity policy"
-  bug: "333444131"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "impulse_velocity_strategy_for_touch_navigation"
   is_exported: true
   namespace: "virtual_devices"
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index a1ae9da..c3c3f0e 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -47,14 +47,6 @@
 
 flag {
      namespace: "virtual_devices"
-     name: "metrics_collection"
-     description: "Enable collection of VDM-related metrics"
-     bug: "324842215"
-     is_fixed_read_only: true
-}
-
-flag {
-     namespace: "virtual_devices"
      name: "activity_control_api"
      description: "Enable APIs for fine grained activity policy, fallback and callbacks"
      bug: "333443509"
@@ -92,27 +84,6 @@
 
 flag {
     namespace: "virtual_devices"
-    name: "virtual_display_multi_window_mode_support"
-    description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default"
-    is_fixed_read_only: true
-    bug: "341151395"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    namespace: "virtual_devices"
-    name: "intent_interception_action_matching_fix"
-    description: "Do not match intents without actions if the filter has actions"
-    bug: "343805037"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    namespace: "virtual_devices"
     name: "virtual_display_rotation_api"
     description: "API for on-demand rotation of virtual displays"
     bug: "291748430"
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index d7195a7..964a8be 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -34,6 +34,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.os.DebugStore;
+
 /**
  * Base class for code that receives and handles broadcast intents sent by
  * {@link android.content.Context#sendBroadcast(Intent)}.
@@ -55,6 +57,9 @@
     private PendingResult mPendingResult;
     private boolean mDebugUnregister;
 
+    private static final boolean DEBUG_STORE_ENABLED =
+            com.android.internal.os.Flags.debugStoreEnabled();
+
     /**
      * State for a result that is pending for a broadcast receiver.  Returned
      * by {@link BroadcastReceiver#goAsync() goAsync()}
@@ -255,6 +260,9 @@
                         "PendingResult#finish#ClassName:" + mReceiverClassName,
                         1);
             }
+            if (DEBUG_STORE_ENABLED) {
+                DebugStore.recordFinish(mReceiverClassName);
+            }
 
             if (mType == TYPE_COMPONENT) {
                 final IActivityManager mgr = ActivityManager.getService();
@@ -433,7 +441,9 @@
     public final PendingResult goAsync() {
         PendingResult res = mPendingResult;
         mPendingResult = null;
-
+        if (DEBUG_STORE_ENABLED) {
+            DebugStore.recordGoAsync(getClass().getName());
+        }
         if (res != null && Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
             res.mReceiverClassName = getClass().getName();
             Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER,
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index d9b0e6d..7c2edd7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -237,6 +237,8 @@
     bug: "307327678"
 }
 
+# This flag is enabled since V but not a MUST requirement in CDD yet, so it needs to stay around
+# for now and any code working with it should keep checking the flag.
 flag {
     name: "restrict_nonpreloads_system_shareduids"
     namespace: "package_manager_service"
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index e0dc568..914aa51 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -34,17 +34,22 @@
 import android.util.LruCache;
 import android.util.Pair;
 import android.util.Printer;
+import com.android.internal.util.RingBuffer;
 import dalvik.system.BlockGuard;
 import dalvik.system.CloseGuard;
+
 import java.io.File;
 import java.io.IOException;
 import java.lang.ref.Reference;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.Locale;
 import java.util.Map;
 import java.util.function.BinaryOperator;
 import java.util.function.UnaryOperator;
@@ -185,7 +190,7 @@
             SQLiteDatabaseConfiguration configuration,
             int connectionId, boolean primaryConnection) {
         mPool = pool;
-        mRecentOperations = new OperationLog(mPool);
+        mRecentOperations = new OperationLog();
         mConfiguration = new SQLiteDatabaseConfiguration(configuration);
         mConnectionId = connectionId;
         mIsPrimaryConnection = primaryConnection;
@@ -305,6 +310,16 @@
         }
     }
 
+    /** Record the start of a transaction for logging and debugging. */
+    void recordBeginTransaction(String mode) {
+        mRecentOperations.beginTransaction(mode);
+    }
+
+    /** Record the end of a transaction for logging and debugging. */
+    void recordEndTransaction(boolean successful) {
+        mRecentOperations.endTransaction(successful);
+    }
+
     private void setPageSize() {
         if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
             final long newValue = SQLiteGlobal.getDefaultPageSize();
@@ -1335,6 +1350,7 @@
         }
         printer.println("  isPrimaryConnection: " + mIsPrimaryConnection);
         printer.println("  onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
+        printer.println("  totalLongOperations: " + mRecentOperations.getTotalLongOperations());
 
         mRecentOperations.dump(printer);
 
@@ -1593,51 +1609,84 @@
         }
     }
 
-    private static final class OperationLog {
+    /**
+     * This class implements a leaky bucket strategy to rate-limit operations.  A client
+     * accumulates one credit every <n> milliseconds; a credit allows the client execute an
+     * operation (which then deducts the credit).  Credits accumulate up to a maximum amount after
+     * which they no longer accumulate.  The strategy allows a client to execute an operation
+     * every <n> milliseconds, or to execute a burst, after a period of no operations.
+     */
+    private static class RateLimiter {
+        // When the bucket was created, in ms.
+        private final long mCreationUptimeMs;
+        // The time required to accumulate a single credit.
+        private final long mMsPerCredit;
+        // The maximum number of credits the process can accumulate.
+        private final int mMaxCredits;
+        // Total credits consumed so far.
+        private long mSpent = 0;
+
+        RateLimiter(long msPerCredit, int maxCredits) {
+            mMsPerCredit = msPerCredit;
+            mMaxCredits = maxCredits;
+            mCreationUptimeMs = SystemClock.uptimeMillis() - (mMsPerCredit * mMaxCredits);
+        }
+
+        /** Return true if there is a credit available (and consume that credit). */
+        boolean tryAcquire() {
+            final long now = SystemClock.uptimeMillis();
+            long credits = (now - mCreationUptimeMs) / mMsPerCredit;
+
+            long available = credits - mSpent;
+            if (available > mMaxCredits) {
+                mSpent += available - mMaxCredits;
+                available = credits - mSpent;
+            }
+            if (available > 0) {
+                mSpent++;
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private final class OperationLog {
         private static final int MAX_RECENT_OPERATIONS = 20;
         private static final int COOKIE_GENERATION_SHIFT = 8;
         private static final int COOKIE_INDEX_MASK = 0xff;
 
+        // Operations over 2s are long.  Save the last ten.
+        private static final long LONG_OPERATION_THRESHOLD_MS = 2_000;
+        private static final int MAX_LONG_OPERATIONS = 10;
+
         private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
-        private int mIndex;
-        private int mGeneration;
-        private final SQLiteConnectionPool mPool;
+        private int mIndex = -1;
+        private int mGeneration = 0;
+        private final Operation mTransaction = new Operation();
         private long mResultLong = Long.MIN_VALUE;
         private String mResultString;
 
-        OperationLog(SQLiteConnectionPool pool) {
-            mPool = pool;
-        }
+        private final RingBuffer<Operation> mLongOperations =
+                new RingBuffer<>(()->{return new Operation();},
+                        (n) ->{return new Operation[n];},
+                        MAX_LONG_OPERATIONS);
+        private int mTotalLongOperations = 0;
+
+        // Limit log messages to one every 5 minutes, except that a burst may be 10 messages long.
+        private final RateLimiter mLongLimiter = new RateLimiter(300_000, 10);
 
         public int beginOperation(String kind, String sql, Object[] bindArgs) {
             mResultLong = Long.MIN_VALUE;
             mResultString = null;
 
             synchronized (mOperations) {
-                final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
-                Operation operation = mOperations[index];
-                if (operation == null) {
-                    operation = new Operation();
-                    mOperations[index] = operation;
-                } else {
-                    operation.mFinished = false;
-                    operation.mException = null;
-                    if (operation.mBindArgs != null) {
-                        operation.mBindArgs.clear();
-                    }
-                }
-                operation.mStartWallTime = System.currentTimeMillis();
-                operation.mStartTime = SystemClock.uptimeMillis();
+                Operation operation = newOperationLocked();
                 operation.mKind = kind;
                 operation.mSql = sql;
-                operation.mPath = mPool.getPath();
-                operation.mResultLong = Long.MIN_VALUE;
-                operation.mResultString = null;
                 if (bindArgs != null) {
                     if (operation.mBindArgs == null) {
                         operation.mBindArgs = new ArrayList<Object>();
-                    } else {
-                        operation.mBindArgs.clear();
                     }
                     for (int i = 0; i < bindArgs.length; i++) {
                         final Object arg = bindArgs[i];
@@ -1649,16 +1698,45 @@
                         }
                     }
                 }
-                operation.mCookie = newOperationCookieLocked(index);
-                if (Trace.isTagEnabled(Trace.TRACE_TAG_DATABASE)) {
+                operation.mTraced = Trace.isTagEnabled(Trace.TRACE_TAG_DATABASE);
+                if (operation.mTraced) {
                     Trace.asyncTraceBegin(Trace.TRACE_TAG_DATABASE, operation.getTraceMethodName(),
                             operation.mCookie);
                 }
-                mIndex = index;
                 return operation.mCookie;
             }
         }
 
+        public void beginTransaction(String kind) {
+            synchronized (mOperations) {
+                Operation operation = newOperationLocked();
+                operation.mKind = kind;
+                mTransaction.copyFrom(operation);
+
+                if (operation.mTraced) {
+                    Trace.asyncTraceBegin(Trace.TRACE_TAG_DATABASE, operation.getTraceMethodName(),
+                            operation.mCookie);
+                }
+            }
+        }
+
+        /**
+         * Fetch a new operation from the ring buffer.  The operation is properly initialized.
+         * This advances mIndex to point to the next element.
+         */
+        private Operation newOperationLocked() {
+            final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
+            Operation operation = mOperations[index];
+            if (operation == null) {
+                mOperations[index] = new Operation();
+                operation = mOperations[index];
+            }
+            operation.start();
+            operation.mCookie = newOperationCookieLocked(index);
+            mIndex = index;
+            return operation;
+        }
+
         public void failOperation(int cookie, Exception ex) {
             synchronized (mOperations) {
                 final Operation operation = getOperationLocked(cookie);
@@ -1682,6 +1760,20 @@
             }
         }
 
+        public boolean endTransaction(boolean success) {
+            synchronized (mOperations) {
+                mTransaction.mResultLong = success ? 1 : 0;
+                final long execTime = finishOperationLocked(mTransaction);
+                final Operation operation = getOperationLocked(mTransaction.mCookie);
+                if (operation != null) {
+                    operation.copyFrom(mTransaction);
+                }
+                mTransaction.setEmpty();
+                return NoPreloadHolder.DEBUG_LOG_SLOW_QUERIES
+                        && SQLiteDebug.shouldLogSlowQuery(execTime);
+            }
+        }
+
         public void logOperation(int cookie, String detail) {
             synchronized (mOperations) {
                 logOperationLocked(cookie, detail);
@@ -1699,13 +1791,11 @@
         private boolean endOperationDeferLogLocked(int cookie) {
             final Operation operation = getOperationLocked(cookie);
             if (operation != null) {
-                if (Trace.isTagEnabled(Trace.TRACE_TAG_DATABASE)) {
+                if (operation.mTraced) {
                     Trace.asyncTraceEnd(Trace.TRACE_TAG_DATABASE, operation.getTraceMethodName(),
                             operation.mCookie);
                 }
-                operation.mEndTime = SystemClock.uptimeMillis();
-                operation.mFinished = true;
-                final long execTime = operation.mEndTime - operation.mStartTime;
+                final long execTime = finishOperationLocked(operation);
                 mPool.onStatementExecuted(execTime);
                 return NoPreloadHolder.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
                         execTime);
@@ -1730,10 +1820,25 @@
             return generation << COOKIE_GENERATION_SHIFT | index;
         }
 
+        /** Close out the operation and return the elapsed time. */
+        private long finishOperationLocked(Operation operation) {
+            operation.mEndTime = SystemClock.uptimeMillis();
+            operation.mFinished = true;
+            final long elapsed = operation.mEndTime - operation.mStartTime;
+            if (elapsed > LONG_OPERATION_THRESHOLD_MS) {
+                mLongOperations.getNextSlot().copyFrom(operation);
+                mTotalLongOperations++;
+                if (mLongLimiter.tryAcquire()) {
+                    Log.i(TAG, "Long db operation: " + mConfiguration.label);
+                }
+            }
+            return elapsed;
+        }
+
         private Operation getOperationLocked(int cookie) {
             final int index = cookie & COOKIE_INDEX_MASK;
             final Operation operation = mOperations[index];
-            return operation.mCookie == cookie ? operation : null;
+            return (operation != null && operation.mCookie == cookie) ? operation : null;
         }
 
         public String describeCurrentOperation() {
@@ -1748,48 +1853,87 @@
             }
         }
 
-        public void dump(Printer printer) {
+        /**
+         * Dump an Operation if it is not in the recent operations list.  Return 1 if the
+         * operation was dumped and 0 if not.
+         */
+        private int dumpIfNotRecentLocked(Printer pw, Operation op, int counter) {
+            if (op == null || op.isEmpty() || getOperationLocked(op.mCookie) != null) {
+                return 0;
+            }
+            pw.println(op.describe(counter));
+            return 1;
+        }
+
+        private void dumpRecentLocked(Printer printer) {
             synchronized (mOperations) {
                 printer.println("  Most recently executed operations:");
                 int index = mIndex;
-                Operation operation = mOperations[index];
-                if (operation != null) {
-                    // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created,
-                    // and is relatively expensive to create during preloading. This method is only
-                    // used when dumping a connection, which is a rare (mainly error) case.
-                    SimpleDateFormat opDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-                    int n = 0;
-                    do {
-                        StringBuilder msg = new StringBuilder();
-                        msg.append("    ").append(n).append(": [");
-                        String formattedStartTime = opDF.format(new Date(operation.mStartWallTime));
-                        msg.append(formattedStartTime);
-                        msg.append("] ");
-                        operation.describe(msg, false); // Never dump bingargs in a bugreport
-                        printer.println(msg.toString());
-
-                        if (index > 0) {
-                            index -= 1;
-                        } else {
-                            index = MAX_RECENT_OPERATIONS - 1;
-                        }
-                        n += 1;
-                        operation = mOperations[index];
-                    } while (operation != null && n < MAX_RECENT_OPERATIONS);
-                } else {
+                if (index == 0) {
                     printer.println("    <none>");
+                    return;
                 }
+
+                // Operations are dumped in order of most recent first.
+                int counter = 0;
+                int n = 0;
+                Operation operation = mOperations[index];
+                do {
+                    printer.println(operation.describe(counter));
+
+                    if (index > 0) {
+                        index -= 1;
+                    } else {
+                        index = MAX_RECENT_OPERATIONS - 1;
+                    }
+                    n++;
+                    counter++;
+                    operation = mOperations[index];
+                } while (operation != null && n < MAX_RECENT_OPERATIONS);
+                counter += dumpIfNotRecentLocked(printer, mTransaction, counter);
+            }
+        }
+
+        private void dumpLongLocked(Printer printer) {
+            printer.println("  Operations exceeding " + LONG_OPERATION_THRESHOLD_MS + "ms:");
+            if (mLongOperations.isEmpty()) {
+                printer.println("    <none>");
+                return;
+            }
+            Operation[] longOps = mLongOperations.toArray();
+            for (int i = 0; i < longOps.length; i++) {
+                if (longOps[i] != null) {
+                    printer.println(longOps[i].describe(i));
+                }
+            }
+        }
+
+        public long getTotalLongOperations() {
+            return mTotalLongOperations;
+        }
+
+        public void dump(Printer printer) {
+            synchronized (mOperations) {
+                dumpRecentLocked(printer);
+                dumpLongLocked(printer);
             }
         }
     }
 
-    private static final class Operation {
+    private final class Operation {
         // Trim all SQL statements to 256 characters inside the trace marker.
         // This limit gives plenty of context while leaving space for other
         // entries in the trace buffer (and ensures atrace doesn't truncate the
         // marker for us, potentially losing metadata in the process).
         private static final int MAX_TRACE_METHOD_NAME_LEN = 256;
 
+        // The reserved start time that indicates the Operation is empty.
+        private static final long EMPTY_OPERATION = -1;
+
+        // The formatter for the timestamp.
+        private static final DateTimeFormatter sDateTime =
+                DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS", Locale.US);
+
         public long mStartWallTime; // in System.currentTimeMillis()
         public long mStartTime; // in SystemClock.uptimeMillis();
         public long mEndTime; // in SystemClock.uptimeMillis();
@@ -1799,16 +1943,61 @@
         public boolean mFinished;
         public Exception mException;
         public int mCookie;
-        public String mPath;
         public long mResultLong; // MIN_VALUE means "value not set".
         public String mResultString;
+        public boolean mTraced;
+
+        /** Reset the object to begin a new operation. */
+        void start() {
+            mStartWallTime = System.currentTimeMillis();
+            mStartTime = SystemClock.uptimeMillis();
+            mEndTime = Long.MIN_VALUE;
+            mKind = null;
+            mSql = null;
+            if (mBindArgs != null) mBindArgs.clear();
+            mFinished = false;
+            mException = null;
+            mCookie = -1;
+            mResultLong = Long.MIN_VALUE;
+            mResultString = null;
+            mTraced = false;
+        }
+
+        /**
+         * Initialize from the source object.  This is meant to clone the object for use in a
+         * transaction operation.  To that end, the local bind args are set to null.
+         */
+        void copyFrom(Operation r) {
+            mStartWallTime = r.mStartWallTime;
+            mStartTime = r.mStartTime;
+            mEndTime = r.mEndTime;
+            mKind = r.mKind;
+            mSql = r.mSql;
+            mBindArgs = null;
+            mFinished = r.mFinished;
+            mException = r.mException;
+            mCookie = r.mCookie;
+            mResultLong = r.mResultLong;
+            mResultString = r.mResultString;
+            mTraced = r.mTraced;
+        }
+
+        /** Mark the operation empty. */
+        void setEmpty() {
+            mStartWallTime = EMPTY_OPERATION;
+        }
+
+        /** Return true if the operation is empty. */
+        boolean isEmpty() {
+            return mStartWallTime == EMPTY_OPERATION;
+        }
 
         public void describe(StringBuilder msg, boolean allowDetailedLog) {
             msg.append(mKind);
             if (mFinished) {
                 msg.append(" took ").append(mEndTime - mStartTime).append("ms");
             } else {
-                msg.append(" started ").append(System.currentTimeMillis() - mStartWallTime)
+                msg.append(" started ").append(SystemClock.uptimeMillis() - mStartTime)
                         .append("ms ago");
             }
             msg.append(" - ").append(getStatus());
@@ -1837,7 +2026,7 @@
                 }
                 msg.append("]");
             }
-            msg.append(", path=").append(mPath);
+            msg.append(", path=").append(mPool.getPath());
             if (mException != null) {
                 msg.append(", exception=\"").append(mException.getMessage()).append("\"");
             }
@@ -1849,6 +2038,21 @@
             }
         }
 
+        /**
+         * Convert a wall-clock time in milliseconds to logcat format.
+         */
+        private String timeString(long millis) {
+            return sDateTime.withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(millis));
+        }
+
+        public String describe(int n) {
+            final StringBuilder msg = new StringBuilder();
+            final String start = timeString(mStartWallTime);
+            msg.append("    ").append(n).append(": [").append(start).append("] ");
+            describe(msg, false); // Never dump bindargs in a bugreport
+            return msg.toString();
+        }
+
         private String getStatus() {
             if (!mFinished) {
                 return "running";
@@ -1862,7 +2066,6 @@
                 return methodName.substring(0, MAX_TRACE_METHOD_NAME_LEN);
             return methodName;
         }
-
     }
 
     /**
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index ad335b6..15d7d66 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -1175,7 +1175,7 @@
                     + ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled
                     + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.resolveJournalMode())
                     + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.resolveSyncMode()));
-            printer.println("  IsReadOnlyDatabase=" + mConfiguration.isReadOnlyDatabase());
+            printer.println("  IsReadOnlyDatabase: " + mConfiguration.isReadOnlyDatabase());
 
             if (isCompatibilityWalEnabled) {
                 printer.println("  Compatibility WAL enabled: wal_syncmode="
diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java
index 7d9f02d..3b14d9d 100644
--- a/core/java/android/database/sqlite/SQLiteSession.java
+++ b/core/java/android/database/sqlite/SQLiteSession.java
@@ -312,6 +312,15 @@
                 cancellationSignal);
     }
 
+    private String modeString(int transactionMode) {
+        switch (transactionMode) {
+            case TRANSACTION_MODE_IMMEDIATE: return "TRANSACTION-IMMEDIATE";
+            case TRANSACTION_MODE_EXCLUSIVE: return "TRANSACTION-EXCLUSIVE";
+            case TRANSACTION_MODE_DEFERRED:  return "TRANSACTION-DEFERRED";
+            default: return "TRANSACTION";
+        }
+    }
+
     private void beginTransactionUnchecked(int transactionMode,
             SQLiteTransactionListener transactionListener, int connectionFlags,
             CancellationSignal cancellationSignal) {
@@ -321,6 +330,7 @@
 
         if (mTransactionStack == null) {
             acquireConnection(null, connectionFlags, cancellationSignal); // might throw
+            mConnection.recordBeginTransaction(modeString(transactionMode));
         }
         try {
             // Set up the transaction such that we can back out safely
@@ -465,6 +475,7 @@
                     mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
                 }
             } finally {
+                mConnection.recordEndTransaction(successful);
                 releaseConnection(); // might throw
             }
         }
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 42f5fc8..9007b62 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -240,18 +240,19 @@
          *
          * @param logoDescription The logo description text that will be shown on the prompt.
          * @return This builder.
-         * @throws IllegalArgumentException If logo description is null or exceeds certain character
-         *                                  limit.
+         * @throws IllegalArgumentException If logo description is null.
          */
         @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
         @NonNull
         public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) {
-            if (logoDescription == null
-                    || logoDescription.length() > MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER) {
-                throw new IllegalArgumentException(
-                        "Logo description passed in can not be null or exceed "
-                                + MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + " character number.");
+            if (logoDescription == null || logoDescription.isEmpty()) {
+                throw new IllegalArgumentException("Logo description passed in can not be null");
+            }
+            if (logoDescription.length() > MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER) {
+                Log.w(TAG,
+                        "Logo description passed in exceeds" + MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER
+                                + " character number and may be truncated.");
             }
             mPromptInfo.setLogoDescription(logoDescription);
             return this;
diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java
index c091062..109b0a8 100644
--- a/core/java/android/hardware/display/BrightnessInfo.java
+++ b/core/java/android/hardware/display/BrightnessInfo.java
@@ -60,7 +60,8 @@
     @IntDef(prefix = {"BRIGHTNESS_MAX_REASON_"}, value = {
             BRIGHTNESS_MAX_REASON_NONE,
             BRIGHTNESS_MAX_REASON_THERMAL,
-            BRIGHTNESS_MAX_REASON_POWER_IC
+            BRIGHTNESS_MAX_REASON_POWER_IC,
+            BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface BrightnessMaxReason {}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index bec1c9e..d85e41d 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -25,6 +25,7 @@
 import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.hardware.input.Flags.touchpadTapDragging;
+import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
 
 import android.Manifest;
@@ -326,6 +327,15 @@
     }
 
     /**
+     * Returns true if the feature flag for touchpad visualizer is enabled.
+     *
+     * @hide
+     */
+    public static boolean isTouchpadVisualizerFeatureFlagEnabled() {
+        return touchpadVisualizer();
+    }
+
+    /**
      * Returns true if the touchpad should allow tap dragging.
      *
      * The returned value only applies to gesture-compatible touchpads.
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index dd484f6..e039953 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -124,8 +124,8 @@
     static final String XML_ATTR_TIME_IN_BACKGROUND = "time_in_background";
     static final String XML_ATTR_TIME_IN_FOREGROUND_SERVICE = "time_in_foreground_service";
 
-    // We need about 700 bytes per UID
-    private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 5_000 * 700;
+    // Max window size. CursorWindow uses only as much memory as needed.
+    private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 20_000_000; // bytes
 
     private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000;
 
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 72b5cf7..90678df 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -214,7 +214,7 @@
     private volatile long mNextInsertSeqValue = 0;
     /*
      * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
-     * Those messages must be in LIFO order - SIGH.
+     * Those messages must be in LIFO order.
      * Decrements on each front of queue insert.
      */
     private static final VarHandle sNextFrontInsertSeq;
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index e6b1c07..14005b3 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -54,7 +54,6 @@
 import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.system.OsConstants;
 import android.system.StructStat;
 import android.text.TextUtils;
 import android.util.DataUnit;
@@ -1535,7 +1534,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModeStringToPosix(String mode) {
         // Quick check for invalid chars
         for (int i = 0; i < mode.length(); i++) {
@@ -1570,7 +1568,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static String translateModePosixToString(int mode) {
         String res = "";
         if ((mode & O_ACCMODE) == O_RDWR) {
@@ -1592,7 +1589,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModePosixToPfd(int mode) {
         int res = 0;
         if ((mode & O_ACCMODE) == O_RDWR) {
@@ -1617,7 +1613,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModePfdToPosix(int mode) {
         int res = 0;
         if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
@@ -1642,7 +1637,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModeAccessToPosix(int mode) {
         if (mode == F_OK) {
             // There's not an exact mapping, so we attempt a read-only open to
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
new file mode 100644
index 0000000..b24e14b
--- /dev/null
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -0,0 +1,1351 @@
+/*
+ * Copyright (C) 2006 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.os;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}.  Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+public final class MessageQueue {
+    private static final String TAG = "LockedMessageQueue";
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
+
+    static final class MessageHeap {
+        static final int MESSAGE_HEAP_INITIAL_SIZE = 16;
+
+        Message[] mHeap = new Message[MESSAGE_HEAP_INITIAL_SIZE];
+        int mNumElements = 0;
+
+        static int parentNodeIdx(int i) {
+            return (i - 1) >>> 1;
+        }
+
+        Message getParentNode(int i) {
+            return mHeap[(i - 1) >>> 1];
+        }
+
+        static int rightNodeIdx(int i) {
+            return 2 * i + 2;
+        }
+
+        Message getRightNode(int i) {
+            return mHeap[2 * i + 2];
+        }
+
+        static int leftNodeIdx(int i) {
+            return 2 * i + 1;
+        }
+
+        Message getLeftNode(int i) {
+            return mHeap[2 * i + 1];
+        }
+
+        int size() {
+            return mHeap.length;
+        }
+
+        int numElements() {
+            return mNumElements;
+        }
+
+        boolean isEmpty() {
+            return mNumElements == 0;
+        }
+
+        Message getMessageAt(int index) {
+            return mHeap[index];
+        }
+
+        /*
+        * Returns:
+        *    0 if x==y.
+        *    A value less than 0 if x<y.
+        *    A value greater than 0 if x>y.
+        */
+        int compareMessage(Message x, Message y) {
+            int compared = Long.compare(x.when, y.when);
+            if (compared == 0) {
+                compared = Long.compare(x.mInsertSeq, y.mInsertSeq);
+            }
+            return compared;
+        }
+
+        int compareMessageByIdx(int x, int y) {
+            return compareMessage(mHeap[x], mHeap[y]);
+        }
+
+        void swap(int x, int y) {
+            Message tmp = mHeap[x];
+            mHeap[x] = mHeap[y];
+            mHeap[y] = tmp;
+        }
+
+        void siftDown(int i) {
+            int smallest = i;
+            int r, l;
+
+            while (true) {
+                r = rightNodeIdx(i);
+                l = leftNodeIdx(i);
+
+                if (r < mNumElements && compareMessageByIdx(r, smallest) < 0) {
+                    smallest = r;
+                }
+
+                if (l < mNumElements && compareMessageByIdx(l, smallest) < 0) {
+                    smallest = l;
+                }
+
+                if (smallest != i) {
+                    swap(i, smallest);
+                    i = smallest;
+                    continue;
+                }
+                break;
+            }
+        }
+
+        boolean siftUp(int i) {
+            boolean swapped = false;
+            while (i != 0 && compareMessage(mHeap[i], getParentNode(i)) < 0) {
+                int p = parentNodeIdx(i);
+
+                swap(i, p);
+                swapped = true;
+                i = p;
+            }
+
+            return swapped;
+        }
+
+        void maybeGrowHeap() {
+            if (mNumElements == mHeap.length) {
+                /* Grow by 1.5x */
+                int newSize = mHeap.length + (mHeap.length >>> 1);
+                Message[] newHeap;
+                if (DEBUG) {
+                    Log.v(TAG, "maybeGrowHeap mNumElements " + mNumElements + " mHeap.length "
+                            + mHeap.length + " newSize " + newSize);
+                }
+
+                newHeap = Arrays.copyOf(mHeap, newSize);
+                mHeap = newHeap;
+            }
+        }
+
+        void add(Message m) {
+            int i;
+
+            maybeGrowHeap();
+
+            i = mNumElements;
+            mNumElements++;
+            mHeap[i] = m;
+
+            siftUp(i);
+        }
+
+        void maybeShrinkHeap() {
+            /* Shrink by 2x */
+            int newSize = mHeap.length >>> 1;
+
+            if (newSize >= MESSAGE_HEAP_INITIAL_SIZE
+                    && mNumElements <= newSize) {
+                Message[] newHeap;
+
+                if (DEBUG) {
+                    Log.v(TAG, "maybeShrinkHeap mNumElements " + mNumElements + " mHeap.length "
+                            + mHeap.length + " newSize " + newSize);
+                }
+
+                newHeap = Arrays.copyOf(mHeap, newSize);
+                mHeap = newHeap;
+            }
+        }
+
+        Message poll() {
+            if (mNumElements > 0) {
+                Message ret = mHeap[0];
+                mNumElements--;
+                mHeap[0] = mHeap[mNumElements];
+                mHeap[mNumElements] = null;
+
+                siftDown(0);
+
+                maybeShrinkHeap();
+                return ret;
+            }
+            return null;
+        }
+
+        Message peek() {
+            if (mNumElements > 0) {
+                return mHeap[0];
+            }
+            return null;
+        }
+
+        private void remove(int i) throws IllegalArgumentException {
+            if (i > mNumElements || mNumElements == 0) {
+                throw new IllegalArgumentException("Index " + i + " out of bounds: "
+                        + mNumElements);
+            } else if (i == (mNumElements - 1)) {
+                mHeap[i] = null;
+                mNumElements--;
+            } else {
+                mNumElements--;
+                mHeap[i] = mHeap[mNumElements];
+                mHeap[mNumElements] = null;
+                if (!siftUp(i)) {
+                    siftDown(i);
+                }
+            }
+            /* Don't shink here, let the caller do this once it has removed all matching items. */
+        }
+
+        void removeAll() {
+            Message m;
+            for (int i = 0; i < mNumElements; i++) {
+                m = mHeap[i];
+                mHeap[i] = null;
+                m.recycleUnchecked();
+            }
+            mNumElements = 0;
+            maybeShrinkHeap();
+        }
+
+        abstract static class MessageHeapCompare {
+            public abstract boolean compareMessage(Message m, Handler h, int what, Object object,
+                    Runnable r, long when);
+        }
+
+        boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+                MessageHeapCompare compare, boolean removeMatches) {
+            boolean found = false;
+            /*
+             * Walk the heap backwards so we don't have to re-visit an array element due to
+             * sifting
+             */
+            for (int i = mNumElements - 1; i >= 0; i--) {
+                if (compare.compareMessage(mHeap[i], h, what, object, r, when)) {
+                    found = true;
+                    if (removeMatches) {
+                        Message m = mHeap[i];
+                        try {
+                            remove(i);
+                        } catch (IllegalArgumentException e) {
+                            Log.wtf(TAG, "Index out of bounds during remove " + e);
+                        }
+                        m.recycleUnchecked();
+                        continue;
+                    }
+                    break;
+                }
+            }
+            if (found && removeMatches) {
+                maybeShrinkHeap();
+            }
+            return found;
+        }
+
+        /*
+        * Keep this for manual debugging. It's easier to pepper the code with this function
+        * than MessageQueue.dump()
+        */
+        void print() {
+            Log.v(TAG, "heap num elem: " + mNumElements + " mHeap.length " + mHeap.length);
+            for (int i = 0; i < mNumElements; i++) {
+                Log.v(TAG, "[" + i + "]\t" + mHeap[i] + " seq: " + mHeap[i].mInsertSeq + " async: "
+                        + mHeap[i].isAsynchronous());
+            }
+        }
+
+        boolean verify(int root) {
+            int r = rightNodeIdx(root);
+            int l = leftNodeIdx(root);
+
+            if (l >= mNumElements && r >= mNumElements) {
+                return true;
+            }
+
+            if (l < mNumElements && compareMessageByIdx(l, root) < 0) {
+                Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
+                        + " left node idx/when: " + l + "/" + mHeap[l].when);
+                return false;
+            }
+
+            if (r < mNumElements && compareMessageByIdx(r, root) < 0) {
+                Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
+                        + " right node idx/when: " + r + "/" + mHeap[r].when);
+                return false;
+            }
+
+            if (!verify(r) || !verify(l)) {
+                return false;
+            }
+            return true;
+        }
+
+        boolean checkDanglingReferences(String where) {
+            /* First, let's make sure we didn't leave any dangling references */
+            for (int i = mNumElements; i < mHeap.length; i++) {
+                if (mHeap[i] != null) {
+                    Log.wtf(TAG, "[" + where
+                            + "] Verify failure: dangling reference found at index "
+                            + i + ": " + mHeap[i] + " Async " + mHeap[i].isAsynchronous()
+                            + " mNumElements " + mNumElements + " mHeap.length " + mHeap.length);
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        boolean verify() {
+            if (!checkDanglingReferences(TAG)) {
+                return false;
+            }
+            return verify(0);
+        }
+    }
+
+    // True if the message queue can be quit.
+    @UnsupportedAppUsage
+    private final boolean mQuitAllowed;
+
+    @UnsupportedAppUsage
+    @SuppressWarnings("unused")
+    private long mPtr; // used by native code
+
+    private final MessageHeap mPriorityQueue = new MessageHeap();
+    private final MessageHeap mAsyncPriorityQueue = new MessageHeap();
+
+    /*
+     * This helps us ensure that messages with the same timestamp are inserted in FIFO order.
+     * Increments on each insert, starting at 0. MessaeHeap.compareMessage() will compare sequences
+     * when delivery timestamps are identical.
+     */
+    private long mNextInsertSeq;
+
+    /*
+     * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
+     * Those messages must be in LIFO order.
+     * Decrements on each front of queue insert.
+     */
+    private long mNextFrontInsertSeq = -1;
+
+    @UnsupportedAppUsage
+    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
+    private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
+    private IdleHandler[] mPendingIdleHandlers;
+    private boolean mQuitting;
+
+    // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
+    private boolean mBlocked;
+
+    // The next barrier token.
+    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
+    @UnsupportedAppUsage
+    private int mNextBarrierToken;
+
+    private native static long nativeInit();
+    private native static void nativeDestroy(long ptr);
+    @UnsupportedAppUsage
+    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    private native static void nativeWake(long ptr);
+    private native static boolean nativeIsPolling(long ptr);
+    private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
+
+    MessageQueue(boolean quitAllowed) {
+        mQuitAllowed = quitAllowed;
+        mPtr = nativeInit();
+    }
+
+    @GuardedBy("this")
+    private void removeRootFromPriorityQueue(Message msg) {
+        Message tmp;
+        if (msg.isAsynchronous()) {
+            tmp = mAsyncPriorityQueue.poll();
+        } else {
+            tmp = mPriorityQueue.poll();
+        }
+        if (DEBUG && tmp != msg) {
+            Log.wtf(TAG, "Unexpected message at head of heap. Wanted: " + msg + " msg.isAsync "
+                    + msg.isAsynchronous() + " Found: " + tmp);
+
+            mPriorityQueue.print();
+            mAsyncPriorityQueue.print();
+        }
+    }
+
+    @GuardedBy("this")
+    private Message pickEarliestMessage(Message x, Message y) {
+        if (x != null && y != null) {
+            if (mPriorityQueue.compareMessage(x, y) < 0) {
+                return x;
+            }
+            return y;
+        }
+
+        return x != null ? x : y;
+    }
+
+    @GuardedBy("this")
+    private Message peekEarliestMessage() {
+        Message x = mPriorityQueue.peek();
+        Message y = mAsyncPriorityQueue.peek();
+
+        return pickEarliestMessage(x, y);
+    }
+
+    @GuardedBy("this")
+    private boolean priorityQueuesAreEmpty() {
+        return mPriorityQueue.isEmpty() && mAsyncPriorityQueue.isEmpty();
+    }
+
+    @GuardedBy("this")
+    private boolean priorityQueueHasBarrier() {
+        Message m = mPriorityQueue.peek();
+
+        if (m != null && m.target == null) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Disposes of the underlying message queue.
+    // Must only be called on the looper thread or the finalizer.
+    private void dispose() {
+        if (mPtr != 0) {
+            nativeDestroy(mPtr);
+            mPtr = 0;
+        }
+    }
+
+    /**
+     * Returns true if the looper has no pending messages which are due to be processed.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is idle.
+     */
+    public boolean isIdle() {
+        synchronized (this) {
+            Message m = peekEarliestMessage();
+            final long now = SystemClock.uptimeMillis();
+
+            return (priorityQueuesAreEmpty() || now < m.when);
+        }
+    }
+
+    /**
+     * Add a new {@link IdleHandler} to this message queue.  This may be
+     * removed automatically for you by returning false from
+     * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+     * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be added.
+     */
+    public void addIdleHandler(@NonNull IdleHandler handler) {
+        if (handler == null) {
+            throw new NullPointerException("Can't add a null IdleHandler");
+        }
+        synchronized (this) {
+            mIdleHandlers.add(handler);
+        }
+    }
+
+    /**
+     * Remove an {@link IdleHandler} from the queue that was previously added
+     * with {@link #addIdleHandler}.  If the given object is not currently
+     * in the idle list, nothing is done.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be removed.
+     */
+    public void removeIdleHandler(@NonNull IdleHandler handler) {
+        synchronized (this) {
+            mIdleHandlers.remove(handler);
+        }
+    }
+
+    /**
+     * Returns whether this looper's thread is currently polling for more work to do.
+     * This is a good signal that the loop is still alive rather than being stuck
+     * handling a callback.  Note that this method is intrinsically racy, since the
+     * state of the loop can change before you get the result back.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is currently polling for events.
+     * @hide
+     */
+    public boolean isPolling() {
+        synchronized (this) {
+            return isPollingLocked();
+        }
+    }
+
+    private boolean isPollingLocked() {
+        // If the loop is quitting then it must not be idling.
+        // We can assume mPtr != 0 when mQuitting is false.
+        return !mQuitting && nativeIsPolling(mPtr);
+    }
+
+    /**
+     * Adds a file descriptor listener to receive notification when file descriptor
+     * related events occur.
+     * <p>
+     * If the file descriptor has already been registered, the specified events
+     * and listener will replace any that were previously associated with it.
+     * It is not possible to set more than one listener per file descriptor.
+     * </p><p>
+     * It is important to always unregister the listener when the file descriptor
+     * is no longer of use.
+     * </p>
+     *
+     * @param fd The file descriptor for which a listener will be registered.
+     * @param events The set of events to receive: a combination of the
+     * {@link OnFileDescriptorEventListener#EVENT_INPUT},
+     * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and
+     * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks.  If the requested
+     * set of events is zero, then the listener is unregistered.
+     * @param listener The listener to invoke when file descriptor events occur.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #removeOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd,
+            @OnFileDescriptorEventListener.Events int events,
+            @NonNull OnFileDescriptorEventListener listener) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        synchronized (this) {
+            updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+        }
+    }
+
+    /**
+     * Removes a file descriptor listener.
+     * <p>
+     * This method does nothing if no listener has been registered for the
+     * specified file descriptor.
+     * </p>
+     *
+     * @param fd The file descriptor whose listener will be unregistered.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #addOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+
+        synchronized (this) {
+            updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events,
+            OnFileDescriptorEventListener listener) {
+        final int fdNum = fd.getInt$();
+
+        int index = -1;
+        FileDescriptorRecord record = null;
+        if (mFileDescriptorRecords != null) {
+            index = mFileDescriptorRecords.indexOfKey(fdNum);
+            if (index >= 0) {
+                record = mFileDescriptorRecords.valueAt(index);
+                if (record != null && record.mEvents == events) {
+                    return;
+                }
+            }
+        }
+
+        if (events != 0) {
+            events |= OnFileDescriptorEventListener.EVENT_ERROR;
+            if (record == null) {
+                if (mFileDescriptorRecords == null) {
+                    mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>();
+                }
+                record = new FileDescriptorRecord(fd, events, listener);
+                mFileDescriptorRecords.put(fdNum, record);
+            } else {
+                record.mListener = listener;
+                record.mEvents = events;
+                record.mSeq += 1;
+            }
+            nativeSetFileDescriptorEvents(mPtr, fdNum, events);
+        } else if (record != null) {
+            record.mEvents = 0;
+            mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
+        }
+    }
+
+    // Called from native code.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int dispatchEvents(int fd, int events) {
+        // Get the file descriptor record and any state that might change.
+        final FileDescriptorRecord record;
+        final int oldWatchedEvents;
+        final OnFileDescriptorEventListener listener;
+        final int seq;
+        synchronized (this) {
+            record = mFileDescriptorRecords.get(fd);
+            if (record == null) {
+                return 0; // spurious, no listener registered
+            }
+
+            oldWatchedEvents = record.mEvents;
+            events &= oldWatchedEvents; // filter events based on current watched set
+            if (events == 0) {
+                return oldWatchedEvents; // spurious, watched events changed
+            }
+
+            listener = record.mListener;
+            seq = record.mSeq;
+        }
+
+        // Invoke the listener outside of the lock.
+        int newWatchedEvents = listener.onFileDescriptorEvents(
+                record.mDescriptor, events);
+        if (newWatchedEvents != 0) {
+            newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR;
+        }
+
+        // Update the file descriptor record if the listener changed the set of
+        // events to watch and the listener itself hasn't been updated since.
+        if (newWatchedEvents != oldWatchedEvents) {
+            synchronized (this) {
+                int index = mFileDescriptorRecords.indexOfKey(fd);
+                if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record
+                        && record.mSeq == seq) {
+                    record.mEvents = newWatchedEvents;
+                    if (newWatchedEvents == 0) {
+                        mFileDescriptorRecords.removeAt(index);
+                    }
+                }
+            }
+        }
+
+        // Return the new set of events to watch for native code to take care of.
+        return newWatchedEvents;
+    }
+
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
+    @UnsupportedAppUsage
+    Message next() {
+        // Return here if the message loop has already quit and been disposed.
+        // This can happen if the application tries to restart a looper after quit
+        // which is not supported.
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        int nextPollTimeoutMillis = 0;
+        for (;;) {
+            if (nextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            nativePollOnce(ptr, nextPollTimeoutMillis);
+
+            synchronized (this) {
+                // Try to retrieve the next message.  Return if found.
+                final long now = SystemClock.uptimeMillis();
+                Message prevMsg = null;
+                Message msg = peekEarliestMessage();
+
+                if (DEBUG && msg != null) {
+                    Log.v(TAG, "Next found message " + msg + " isAsynchronous: "
+                            + msg.isAsynchronous() + " target " + msg.target);
+                }
+
+                if (msg != null && !msg.isAsynchronous() && msg.target == null) {
+                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
+                    msg = mAsyncPriorityQueue.peek();
+                    if (DEBUG) {
+                        Log.v(TAG, "Next message was barrier async msg: " + msg);
+                    }
+                }
+
+                if (msg != null) {
+                    if (now < msg.when) {
+                        // Next message is not ready.  Set a timeout to wake up when it is ready.
+                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
+                    } else {
+                        mBlocked = false;
+                        removeRootFromPriorityQueue(msg);
+                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
+                        msg.markInUse();
+                        if (TRACE) {
+                            Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                        }
+                        return msg;
+                    }
+                } else {
+                    // No more messages.
+                    nextPollTimeoutMillis = -1;
+                }
+
+                // Process the quit message now that all pending messages have been handled.
+                if (mQuitting) {
+                    dispose();
+                    return null;
+                }
+
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                Message next = peekEarliestMessage();
+                if (pendingIdleHandlerCount < 0
+                        && (next == null || now < next.when)) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    mBlocked = true;
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (this) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            nextPollTimeoutMillis = 0;
+        }
+    }
+
+    void quit(boolean safe) {
+        if (!mQuitAllowed) {
+            throw new IllegalStateException("Main thread not allowed to quit.");
+        }
+
+        synchronized (this) {
+            if (mQuitting) {
+                return;
+            }
+            mQuitting = true;
+
+            if (safe) {
+                removeAllFutureMessagesLocked();
+            } else {
+                removeAllMessagesLocked();
+            }
+
+            // We can assume mPtr != 0 because mQuitting was previously false.
+            nativeWake(mPtr);
+        }
+    }
+
+    /**
+     * Posts a synchronization barrier to the Looper's message queue.
+     *
+     * Message processing occurs as usual until the message queue encounters the
+     * synchronization barrier that has been posted.  When the barrier is encountered,
+     * later synchronous messages in the queue are stalled (prevented from being executed)
+     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
+     * the token that identifies the synchronization barrier.
+     *
+     * This method is used to immediately postpone execution of all subsequently posted
+     * synchronous messages until a condition is met that releases the barrier.
+     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
+     * and continue to be processed as usual.
+     *
+     * This call must be always matched by a call to {@link #removeSyncBarrier} with
+     * the same token to ensure that the message queue resumes normal operation.
+     * Otherwise the application will probably hang!
+     *
+     * @return A token that uniquely identifies the barrier.  This token must be
+     * passed to {@link #removeSyncBarrier} to release the barrier.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public int postSyncBarrier() {
+        return postSyncBarrier(SystemClock.uptimeMillis());
+    }
+
+    private int postSyncBarrier(long when) {
+        // Enqueue a new sync barrier token.
+        // We don't need to wake the queue because the purpose of a barrier is to stall it.
+        synchronized (this) {
+            final int token = mNextBarrierToken++;
+            final Message msg = Message.obtain();
+            msg.arg1 = token;
+
+            enqueueMessageUnchecked(msg, when);
+            return token;
+        }
+    }
+
+    private class MatchBarrierToken extends MessageHeap.MessageHeapCompare {
+        int mBarrierToken;
+
+        MatchBarrierToken(int token) {
+            super();
+            mBarrierToken = token;
+        }
+
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == null && m.arg1 == mBarrierToken) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Removes a synchronization barrier.
+     *
+     * @param token The synchronization barrier token that was returned by
+     * {@link #postSyncBarrier}.
+     *
+     * @throws IllegalStateException if the barrier was not found.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public void removeSyncBarrier(int token) {
+        final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token);
+
+        // Remove a sync barrier token from the queue.
+        // If the queue is no longer stalled by a barrier then wake it.
+        synchronized (this) {
+            boolean removed;
+            Message first = mPriorityQueue.peek();
+
+            removed = mPriorityQueue.findOrRemoveMessages(null, 0, null, null, 0,
+                    matchBarrierToken, true);
+            if (removed && first != null) {
+                // If the loop is quitting then it is already awake.
+                // We can assume mPtr != 0 when mQuitting is false.
+                if (first.target == null && first.arg1 == token && !mQuitting) {
+                    nativeWake(mPtr);
+                }
+            } else if (!removed) {
+                throw new IllegalStateException("The specified message queue synchronization "
+                        + " barrier token has not been posted or has already been removed.");
+            }
+        }
+    }
+
+    boolean enqueueMessage(Message msg, long when) {
+        if (msg.target == null) {
+            throw new IllegalArgumentException("Message must have a target.");
+        }
+
+        return enqueueMessageUnchecked(msg, when);
+    }
+
+    boolean enqueueMessageUnchecked(Message msg, long when) {
+        synchronized (this) {
+            if (mQuitting) {
+                IllegalStateException e = new IllegalStateException(
+                        msg.target + " sending message to a Handler on a dead thread");
+                Log.w(TAG, e.getMessage(), e);
+                msg.recycle();
+                return false;
+            }
+
+            if (msg.isInUse()) {
+                throw new IllegalStateException(msg + " This message is already in use.");
+            }
+
+            msg.markInUse();
+            msg.when = when;
+            msg.mInsertSeq = when != 0 ? mNextInsertSeq++ : mNextFrontInsertSeq--;
+            if (DEBUG) Log.v(TAG, "Enqueue message: " + msg);
+            boolean needWake;
+            boolean isBarrier = msg.target == null;
+            Message first = peekEarliestMessage();
+
+            if (priorityQueuesAreEmpty() || when == 0 || when < first.when) {
+                needWake = mBlocked && !isBarrier;
+            } else {
+                Message firstNonAsyncMessage =
+                        first.isAsynchronous() ? mPriorityQueue.peek() : first;
+
+                needWake = mBlocked && firstNonAsyncMessage != null
+                        && firstNonAsyncMessage.target == null && msg.isAsynchronous();
+            }
+
+            if (msg.isAsynchronous()) {
+                mAsyncPriorityQueue.add(msg);
+            } else {
+                mPriorityQueue.add(msg);
+            }
+
+            // We can assume mPtr != 0 because mQuitting is false.
+            if (needWake) {
+                nativeWake(mPtr);
+            }
+        }
+        return true;
+    }
+
+    @GuardedBy("this")
+    boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+                MessageHeap.MessageHeapCompare compare, boolean removeMatches) {
+        boolean found = mPriorityQueue.findOrRemoveMessages(h, what, object, r, when, compare,
+                removeMatches);
+        boolean foundAsync = mAsyncPriorityQueue.findOrRemoveMessages(h, what, object, r, when,
+                compare, removeMatches);
+        return found || foundAsync;
+    }
+
+    private static class MatchHandlerWhatAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerWhatAndObject sMatchHandlerWhatAndObject =
+            new MatchHandlerWhatAndObject();
+
+    boolean hasMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject,
+                    false);
+        }
+    }
+
+    private static class MatchHandlerWhatAndObjectEquals extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerWhatAndObjectEquals sMatchHandlerWhatAndObjectEquals =
+            new MatchHandlerWhatAndObjectEquals();
+    boolean hasEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, what, object, null, 0,
+                    sMatchHandlerWhatAndObjectEquals, false);
+        }
+    }
+
+    private static class MatchHandlerRunnableAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerRunnableAndObject sMatchHandlerRunnableAndObject =
+            new MatchHandlerRunnableAndObject();
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    boolean hasMessages(Handler h, Runnable r, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject,
+                    false);
+        }
+    }
+
+    private static class MatchHandler extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandler sMatchHandler = new MatchHandler();
+    boolean hasMessages(Handler h) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, -1, null, null, 0, sMatchHandler, false);
+        }
+    }
+
+    void removeMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject, true);
+        }
+    }
+
+    void removeEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObjectEquals, true);
+        }
+    }
+
+    void removeMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject, true);
+        }
+    }
+
+    private static class MatchHandlerRunnableAndObjectEquals
+            extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerRunnableAndObjectEquals sMatchHandlerRunnableAndObjectEquals =
+            new MatchHandlerRunnableAndObjectEquals();
+    void removeEqualMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObjectEquals, true);
+        }
+    }
+
+    private static class MatchHandlerAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerAndObject sMatchHandlerAndObject = new MatchHandlerAndObject();
+    void removeCallbacksAndMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObject, true);
+        }
+    }
+
+    private static class MatchHandlerAndObjectEquals extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerAndObjectEquals sMatchHandlerAndObjectEquals =
+            new MatchHandlerAndObjectEquals();
+    void removeCallbacksAndEqualMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObjectEquals, true);
+        }
+    }
+
+    @GuardedBy("this")
+    private void removeAllMessagesLocked() {
+        mPriorityQueue.removeAll();
+        mAsyncPriorityQueue.removeAll();
+    }
+
+    private static class MatchAllFutureMessages extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when > when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchAllFutureMessages sMatchAllFutureMessages =
+            new MatchAllFutureMessages();
+    @GuardedBy("this")
+    private void removeAllFutureMessagesLocked() {
+        findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(),
+                sMatchAllFutureMessages, true);
+    }
+
+    int dumpPriorityQueue(Printer pw, String prefix, Handler h, MessageHeap priorityQueue) {
+        int n = 0;
+        long now = SystemClock.uptimeMillis();
+        for (int i = 0; i < priorityQueue.numElements(); i++) {
+            Message m = priorityQueue.getMessageAt(i);
+            if (h == null && h == m.target) {
+                pw.println(prefix + "Message " + n + ": " + m.toString(now));
+                n++;
+            }
+        }
+        return n;
+    }
+
+    void dumpPriorityQueue(ProtoOutputStream proto, MessageHeap priorityQueue) {
+        for (int i = 0; i < priorityQueue.numElements(); i++) {
+            Message m = priorityQueue.getMessageAt(i);
+            m.dumpDebug(proto, MessageQueueProto.MESSAGES);
+        }
+    }
+
+    void dump(Printer pw, String prefix, Handler h) {
+        synchronized (this) {
+            pw.println(prefix + "(MessageQueue is using Locked implementation)");
+            long now = SystemClock.uptimeMillis();
+            int n = dumpPriorityQueue(pw, prefix, h, mPriorityQueue);
+            n += dumpPriorityQueue(pw, prefix, h, mAsyncPriorityQueue);
+            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+                    + ", quitting=" + mQuitting + ")");
+        }
+    }
+
+    void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        final long messageQueueToken = proto.start(fieldId);
+        synchronized (this) {
+            dumpPriorityQueue(proto, mPriorityQueue);
+            dumpPriorityQueue(proto, mAsyncPriorityQueue);
+            proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPollingLocked());
+            proto.write(MessageQueueProto.IS_QUITTING, mQuitting);
+        }
+        proto.end(messageQueueToken);
+    }
+
+    /**
+     * Callback interface for discovering when a thread is going to block
+     * waiting for more messages.
+     */
+    public static interface IdleHandler {
+        /**
+         * Called when the message queue has run out of messages and will now
+         * wait for more.  Return true to keep your idle handler active, false
+         * to have it removed.  This may be called if there are still messages
+         * pending in the queue, but they are all scheduled to be dispatched
+         * after the current time.
+         */
+        boolean queueIdle();
+    }
+
+    /**
+     * A listener which is invoked when file descriptor related events occur.
+     */
+    public interface OnFileDescriptorEventListener {
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for input
+         * operations, such as reading.
+         * <p>
+         * The listener should read all available data from the file descriptor
+         * then return <code>true</code> to keep the listener active or <code>false</code>
+         * to remove the listener.
+         * </p><p>
+         * In the case of a socket, this event may be generated to indicate
+         * that there is at least one incoming connection that the listener
+         * should accept.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_INPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_INPUT = 1 << 0;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for output
+         * operations, such as writing.
+         * <p>
+         * The listener should write as much data as it needs.  If it could not
+         * write everything at once, then it should return <code>true</code> to
+         * keep the listener active.  Otherwise, it should return <code>false</code>
+         * to remove the listener then re-register it later when it needs to write
+         * something else.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_OUTPUT = 1 << 1;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor encountered a
+         * fatal error.
+         * <p>
+         * File descriptor errors can occur for various reasons.  One common error
+         * is when the remote peer of a socket or pipe closes its end of the connection.
+         * </p><p>
+         * This event may be generated at any time regardless of whether the
+         * {@link #EVENT_ERROR} event mask was specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_ERROR = 1 << 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+                EVENT_INPUT,
+                EVENT_OUTPUT,
+                EVENT_ERROR
+        })
+        public @interface Events {}
+
+        /**
+         * Called when a file descriptor receives events.
+         *
+         * @param fd The file descriptor.
+         * @param events The set of events that occurred: a combination of the
+         * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks.
+         * @return The new set of events to watch, or 0 to unregister the listener.
+         *
+         * @see #EVENT_INPUT
+         * @see #EVENT_OUTPUT
+         * @see #EVENT_ERROR
+         */
+        @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events);
+    }
+
+    private static final class FileDescriptorRecord {
+        public final FileDescriptor mDescriptor;
+        public int mEvents;
+        public OnFileDescriptorEventListener mListener;
+        public int mSeq;
+
+        public FileDescriptorRecord(FileDescriptor descriptor,
+                int events, OnFileDescriptorEventListener listener) {
+            mDescriptor = descriptor;
+            mEvents = events;
+            mListener = listener;
+        }
+    }
+}
diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java
index 161951e..a1db9be 100644
--- a/core/java/android/os/Message.java
+++ b/core/java/android/os/Message.java
@@ -126,6 +126,10 @@
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public long when;
 
+    /** @hide */
+    @SuppressWarnings("unused")
+    public long mInsertSeq;
+
     /*package*/ Bundle data;
 
     @UnsupportedAppUsage
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 464df23..4cc057a 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -340,7 +340,6 @@
         return pfd;
     }
 
-    @RavenwoodReplace
     private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException {
         if ((mode & MODE_WRITE_ONLY) != 0 && (mode & MODE_APPEND) == 0
                 && (mode & MODE_TRUNCATE) == 0 && ((mode & MODE_READ_ONLY) == 0)
@@ -364,26 +363,16 @@
         }
     }
 
-    private static FileDescriptor openInternal$ravenwood(File file, int mode)
-            throws FileNotFoundException {
-        try {
-            return native_open$ravenwood(file, mode);
-        } catch (FileNotFoundException e) {
-            throw e;
-        } catch (IOException e) {
-            throw new FileNotFoundException(e.getMessage());
-        }
-    }
-
     @RavenwoodReplace
     private static void closeInternal(FileDescriptor fd) {
         IoUtils.closeQuietly(fd);
     }
 
     private static void closeInternal$ravenwood(FileDescriptor fd) {
-        // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood
-        // side to close it.
-        native_close$ravenwood(fd);
+        try {
+            Os.close(fd);
+        } catch (ErrnoException ignored) {
+        }
     }
 
     /**
@@ -743,7 +732,6 @@
      * Return the total size of the file representing this fd, as determined by
      * {@code stat()}. Returns -1 if the fd is not a file.
      */
-    @RavenwoodThrow(reason = "Os.readlink() and Os.stat()")
     public long getStatSize() {
         if (mWrapped != null) {
             return mWrapped.getStatSize();
@@ -1277,32 +1265,19 @@
         }
     }
 
-    // These native methods are currently only implemented by Ravenwood, as it's the only
-    // mechanism we have to jump to our RavenwoodNativeSubstitutionClass
-    private static native void native_setFdInt$ravenwood(FileDescriptor fd, int fdInt);
-    private static native int native_getFdInt$ravenwood(FileDescriptor fd);
-    private static native FileDescriptor native_open$ravenwood(File file, int pfdMode)
-            throws IOException;
-    private static native void native_close$ravenwood(FileDescriptor fd);
+    private static native void setFdInt$ravenwood(FileDescriptor fd, int fdInt);
+    private static native int getFdInt$ravenwood(FileDescriptor fd);
 
     @RavenwoodReplace
     private static void setFdInt(FileDescriptor fd, int fdInt) {
         fd.setInt$(fdInt);
     }
 
-    private static void setFdInt$ravenwood(FileDescriptor fd, int fdInt) {
-        native_setFdInt$ravenwood(fd, fdInt);
-    }
-
     @RavenwoodReplace
     private static int getFdInt(FileDescriptor fd) {
         return fd.getInt$();
     }
 
-    private static int getFdInt$ravenwood(FileDescriptor fd) {
-        return native_getFdInt$ravenwood(fd);
-    }
-
     @RavenwoodReplace
     private void setFdOwner(FileDescriptor fd) {
         IoUtils.setFdOwner(fd, this);
@@ -1320,7 +1295,6 @@
     private int acquireRawFd$ravenwood(FileDescriptor fd) {
         // FD owners currently unsupported under Ravenwood; return FD directly
         return getFdInt(fd);
-
     }
 
     @RavenwoodReplace
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 967332f..79f229a 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -209,7 +209,7 @@
     private volatile long mNextInsertSeqValue = 0;
     /*
      * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
-     * Those messages must be in LIFO order - SIGH.
+     * Those messages must be in LIFO order.
      * Decrements on each front of queue insert.
      */
     private static final VarHandle sNextFrontInsertSeq;
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 5ef597d..3fe063d 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -91,6 +91,8 @@
     bug: "283989236"
 }
 
+# This flag is enabled since V but not a MUST requirement in CDD yet, so it needs to stay around
+# for now and any code working with it should keep checking the flag.
 flag {
     name: "signature_permission_allowlist_enabled"
     is_fixed_read_only: true
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0ee6f43..5703f69 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5912,6 +5912,14 @@
         public static final String SHOW_KEY_PRESSES = "show_key_presses";
 
         /**
+         * Show touchpad input visualization on screen.
+         * 0 = no
+         * 1 = yes
+         * @hide
+         */
+        public static final String TOUCHPAD_VISUALIZER = "touchpad_visualizer";
+
+        /**
          * Show rotary input dispatched to focused windows on the screen.
          * 0 = no
          * 1 = yes
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index e16a6a1..7ca248d 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -211,6 +211,9 @@
             SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
                     | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT;
 
+    private static final int LEGACY_SUPPRESSED_EFFECTS =
+            Policy.SUPPRESSED_EFFECT_SCREEN_ON | Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+
     // ZenModeConfig XML versions distinguishing key changes.
     public static final int XML_VERSION_ZEN_UPGRADE = 8;
     public static final int XML_VERSION_MODES_API = 11;
@@ -284,6 +287,7 @@
     private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
     private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
     private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
+    private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
 
     private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
     private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -1171,6 +1175,8 @@
             if (Flags.modesUi()) {
                 rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
                         UPDATE_ORIGIN_UNKNOWN);
+                rt.legacySuppressedEffects = safeInt(parser,
+                        RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
             }
         }
         return rt;
@@ -1228,6 +1234,8 @@
             }
             if (Flags.modesUi()) {
                 out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+                out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
+                        rule.legacySuppressedEffects);
             }
         }
     }
@@ -1903,6 +1911,13 @@
                             ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST))) {
                 suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
             }
+
+            // Restore legacy suppressed effects (obsolete fields which are not in ZenPolicy).
+            // These are deprecated and have no effect on behavior, however apps should get them
+            // back if provided to setNotificationPolicy() earlier.
+            suppressedVisualEffects &= ~LEGACY_SUPPRESSED_EFFECTS;
+            suppressedVisualEffects |=
+                    (LEGACY_SUPPRESSED_EFFECTS & manualRule.legacySuppressedEffects);
         } else {
             if (isAllowConversations()) {
                 priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
@@ -1996,6 +2011,8 @@
         if (policy == null) return;
         if (Flags.modesUi()) {
             manualRule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+            manualRule.legacySuppressedEffects =
+                    LEGACY_SUPPRESSED_EFFECTS & policy.suppressedVisualEffects;
         } else {
             setAllowAlarms((policy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS) != 0);
             allowMedia = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MEDIA) != 0;
@@ -2521,6 +2538,10 @@
         @Nullable public Instant deletionInstant; // Only set on deleted rules.
         @FlaggedApi(Flags.FLAG_MODES_UI)
         @ConfigChangeOrigin public int disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
+        // The obsolete suppressed effects in NM.Policy (SCREEN_ON, SCREEN_OFF) cannot be put in a
+        // ZenPolicy, so we store them here, only for the manual rule.
+        @FlaggedApi(Flags.FLAG_MODES_UI)
+        int legacySuppressedEffects;
 
         public ZenRule() { }
 
@@ -2561,6 +2582,7 @@
                 }
                 if (Flags.modesUi()) {
                     disabledOrigin = source.readInt();
+                    legacySuppressedEffects = source.readInt();
                 }
             }
         }
@@ -2638,6 +2660,7 @@
                 }
                 if (Flags.modesUi()) {
                     dest.writeInt(disabledOrigin);
+                    dest.writeInt(legacySuppressedEffects);
                 }
             }
         }
@@ -2686,6 +2709,7 @@
                 }
                 if (Flags.modesUi()) {
                     sb.append(",disabledOrigin=").append(disabledOrigin);
+                    sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
                 }
             }
 
@@ -2754,7 +2778,8 @@
 
                 if (Flags.modesUi()) {
                     finalEquals = finalEquals
-                            && other.disabledOrigin == disabledOrigin;
+                            && other.disabledOrigin == disabledOrigin
+                            && other.legacySuppressedEffects == legacySuppressedEffects;
                 }
             }
 
@@ -2769,15 +2794,15 @@
                             component, configurationActivity, pkg, id, enabler, zenPolicy,
                             zenDeviceEffects, modified, allowManualInvocation, iconResName,
                             triggerDescription, type, userModifiedFields,
-                            zenPolicyUserModifiedFields,
-                            zenDeviceEffectsUserModifiedFields, deletionInstant, disabledOrigin);
+                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                            deletionInstant, disabledOrigin, legacySuppressedEffects);
                 } 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);
+                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                            deletionInstant);
                 }
             }
             return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 91ef11c..a37e227 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -472,6 +472,7 @@
         public static final String FIELD_ICON_RES = "iconResName";
         public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
         public static final String FIELD_TYPE = "type";
+        public static final String FIELD_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
         // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
 
         // Special field to track whether this rule became active or inactive
@@ -567,6 +568,13 @@
                 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));
+                    }
+                }
             }
         }
 
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 634469d..a7641c0 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -164,6 +164,9 @@
             float width, float height, float vecX, float vecY,
             float maxStretchAmountX, float maxStretchAmountY, float childRelativeLeft,
             float childRelativeTop, float childRelativeRight, float childRelativeBottom);
+    private static native void nativeSetEdgeExtensionEffect(long transactionObj, long nativeObj,
+                                                            boolean leftEdge, boolean rightEdge,
+                                                            boolean topEdge, boolean bottomEdge);
     private static native void nativeSetTrustedOverlay(long transactionObj, long nativeObject,
             int isTrustedOverlay);
     private static native void nativeSetDropInputMode(
@@ -3513,6 +3516,19 @@
         /**
          * @hide
          */
+        public Transaction setEdgeExtensionEffect(SurfaceControl sc, int edge) {
+            checkPreconditions(sc);
+
+            nativeSetEdgeExtensionEffect(
+                    mNativeObject, sc.mNativeObject,
+                    (edge & WindowInsets.Side.LEFT) != 0, (edge & WindowInsets.Side.RIGHT) != 0,
+                    (edge & WindowInsets.Side.TOP) != 0, (edge & WindowInsets.Side.BOTTOM) != 0);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.O)
         public Transaction setLayerStack(SurfaceControl sc, int layerStack) {
             checkPreconditions(sc);
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index a806bd2..121c01b 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -73,7 +73,7 @@
             }
             // Sort entries by time registered when dumping
             // TODO: Or should it sort by name?
-            entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue()));
+            entries.sort((o1, o2) -> Long.compare(o1.getValue(), o2.getValue()));
             final int size = Math.min(entries.size(), limit);
 
             pw.println("SurfaceControlRegistry");
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 14978ed..85d4ec0 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -478,6 +478,12 @@
      */
     int TRANSIT_SLEEP = 12;
     /**
+     * An Activity was going to be visible from back navigation.
+     * @hide
+     */
+    int TRANSIT_PREPARE_BACK_NAVIGATION = 13;
+
+    /**
      * The first slot for custom transition types. Callers (like Shell) can make use of custom
      * transition types for dealing with special cases. These types are effectively ignored by
      * Core and will just be passed along as part of TransitionInfo objects. An example is
@@ -505,6 +511,7 @@
             TRANSIT_PIP,
             TRANSIT_WAKE,
             TRANSIT_SLEEP,
+            TRANSIT_PREPARE_BACK_NAVIGATION,
             TRANSIT_FIRST_CUSTOM
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -1918,6 +1925,7 @@
             case TRANSIT_PIP: return "PIP";
             case TRANSIT_WAKE: return "WAKE";
             case TRANSIT_SLEEP: return "SLEEP";
+            case TRANSIT_PREPARE_BACK_NAVIGATION: return "PREDICTIVE_BACK";
             case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
             default:
                 if (type > TRANSIT_FIRST_CUSTOM) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index d40b72c..4ab6758 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -973,8 +973,12 @@
 
         @GuardedBy("mH")
         private void setCurrentRootViewLocked(ViewRootImpl rootView) {
+            final boolean wasEmpty = mCurRootView == null;
             mImeDispatcher.switchRootView(mCurRootView, rootView);
             mCurRootView = rootView;
+            if (wasEmpty && mCurRootView != null) {
+                mImeDispatcher.updateReceivingDispatcher(mCurRootView.getOnBackInvokedDispatcher());
+            }
         }
     }
 
@@ -2949,7 +2953,8 @@
      * @param callback Consumer callback that provides {@code true} if view belongs to allowed
      *                delegate package declared in
      *                {@link #prepareStylusHandwritingDelegation(View, String)} and handwriting
-     *                session can start.
+     *                session can start. Note: The caller should hold a reference to the callback.
+     *                The framework only holds a weak reference.
      * @see #prepareStylusHandwritingDelegation(View, String)
      * @see #acceptStylusHandwritingDelegation(View)
      */
@@ -2979,7 +2984,8 @@
      * @param delegatorPackageName package name of the delegator that handled initial stylus stroke.
      * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0}
      * @param executor The executor to run the callback on.
-     * @param callback {@code true>} would be received if delegation was accepted.
+     * @param callback {@code true} would be received if delegation was accepted. The caller should
+     *                 hold a reference to the callback. The framework only holds a weak reference.
      * @see #prepareStylusHandwritingDelegation(View, String)
      * @see #acceptStylusHandwritingDelegation(View)
      */
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9099db8..ac899f4 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -31,6 +31,7 @@
 
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
 
 import android.R;
 import android.annotation.CallSuper;
@@ -2738,6 +2739,8 @@
 
         InputMethodManager imm = getInputMethodManager();
         if (imm != null) imm.restartInput(this);
+
+        ensureEditorFocusedNotifiedToHandwritingInitiator();
     }
 
     private void setInputTypeFromEditor() {
@@ -7843,6 +7846,20 @@
         if (type == InputType.TYPE_NULL && mEditor == null) return; //TYPE_NULL is the default value
         createEditorIfNeeded();
         mEditor.mInputType = type;
+        ensureEditorFocusedNotifiedToHandwritingInitiator();
+    }
+
+    private void ensureEditorFocusedNotifiedToHandwritingInitiator() {
+        if (!initiationWithoutInputConnection() || isHandwritingDelegate()) {
+            return;
+        }
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            return;
+        }
+        if (isFocused() && hasWindowFocus() && onCheckIsTextEditor()) {
+            viewRoot.getHandwritingInitiator().onEditorFocused(this);
+        }
     }
 
     @Override
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index 9cd2a71..62a7283 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -23,7 +23,6 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.WindowConfiguration;
-import android.companion.virtualdevice.flags.Flags;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -67,9 +66,7 @@
     public DisplayWindowPolicyController() {
         synchronized (mSupportedWindowingModes) {
             mSupportedWindowingModes.add(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
-            if (Flags.virtualDisplayMultiWindowModeSupport()) {
-                mSupportedWindowingModes.add(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
-            }
+            mSupportedWindowingModes.add(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
         }
     }
 
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index ce1f986..771dc7a 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -27,10 +27,12 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.Log;
+import android.util.Pair;
 import android.view.ViewRootImpl;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.function.Consumer;
 
@@ -58,7 +60,7 @@
     // The handler to run callbacks on. This should be on the same thread
     // the ViewRootImpl holding IME's WindowOnBackInvokedDispatcher is created on.
     private Handler mHandler;
-
+    private final ArrayDeque<Pair<Integer, Bundle>> mQueuedReceive = new ArrayDeque<>();
     public ImeOnBackInvokedDispatcher(Handler handler) {
         mResultReceiver = new ResultReceiver(handler) {
             @Override
@@ -66,11 +68,22 @@
                 WindowOnBackInvokedDispatcher dispatcher = getReceivingDispatcher();
                 if (dispatcher != null) {
                     receive(resultCode, resultData, dispatcher);
+                } else {
+                    mQueuedReceive.add(new Pair<>(resultCode, resultData));
                 }
             }
         };
     }
 
+    /** Set receiving dispatcher to consume queued receiving events. */
+    public void updateReceivingDispatcher(@NonNull WindowOnBackInvokedDispatcher dispatcher) {
+        while (!mQueuedReceive.isEmpty()) {
+            final Pair<Integer, Bundle> queuedMessage = mQueuedReceive.poll();
+            receive(queuedMessage.first, queuedMessage.second, dispatcher);
+        }
+    }
+
+
     void setHandler(@NonNull Handler handler) {
         mHandler = handler;
     }
@@ -198,6 +211,7 @@
             }
         }
         mImeCallbacks.clear();
+        mQueuedReceive.clear();
     }
 
     @VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2d5b02a..e5ef95c 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -204,3 +204,10 @@
     description: "Enables the tracking of the status for compat ui elements."
     bug: "350953004"
 }
+
+flag {
+    name: "enable_desktop_windowing_app_to_web_education"
+    namespace: "lse_desktop_experience"
+    description: "Enables desktop windowing app-to-web education"
+    bug: "348205896"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 80101af..d5746e5 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -94,14 +94,6 @@
 }
 
 flag {
-    name: "activity_snapshot_by_default"
-    namespace: "systemui"
-    description: "Enable record activity snapshot by default"
-    bug: "259497289"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "supports_multi_instance_system_ui"
     is_exported: true
     namespace: "multitasking"
@@ -209,3 +201,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+flag {
+  name: "migrate_predictive_back_transition"
+  namespace: "windowing_frontend"
+  description: "Create transition when visibility change from predictive back"
+  bug: "347168362"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 4c18bbf..b8c2a5f 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -135,14 +135,3 @@
         purpose: PURPOSE_BUGFIX
     }
 }
-
-flag {
-    namespace: "windowing_sdk"
-    name: "per_user_display_window_settings"
-    description: "Whether to store display window settings per user to avoid conflicts"
-    bug: "346668297"
-    is_fixed_read_only: true
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 6258f5c..ca4d1b6 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2960,10 +2960,10 @@
     }
 
     private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
-        return shouldShowTabs()
-                && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
-                        UserHandle.of(UserHandle.myUserId())).getCount() > 0
-                    || shouldShowStickyContentPreviewWhenEmpty())
+        ResolverListAdapter adapter = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+                UserHandle.of(UserHandle.myUserId()));
+        boolean isEmpty = adapter == null || adapter.getCount() == 0;
+        return shouldShowTabs() && (!isEmpty || shouldShowStickyContentPreviewWhenEmpty())
                 && shouldShowContentPreview();
     }
 
diff --git a/core/java/com/android/internal/os/DebugStore.java b/core/java/com/android/internal/os/DebugStore.java
new file mode 100644
index 0000000..4c45fee
--- /dev/null
+++ b/core/java/com/android/internal/os/DebugStore.java
@@ -0,0 +1,247 @@
+/*
+ * 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.internal.os;
+
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * The DebugStore class provides methods for recording various debug events related to service
+ * lifecycle, broadcast receivers and others.
+ * The DebugStore class facilitates debugging ANR issues by recording time-stamped events
+ * related to service lifecycles, broadcast receivers, and other framework operations. It logs
+ * the start and end times of operations within the ANR timer scope called  by framework,
+ * enabling pinpointing of methods and events contributing to ANRs.
+ *
+ * Usage currently includes recording service starts, binds, and asynchronous operations initiated
+ * by broadcast receivers, providing a granular view of system behavior that facilitates
+ * identifying performance bottlenecks and optimizing issue resolution.
+ *
+ * @hide
+ */
+public class DebugStore {
+    private static DebugStoreNative sDebugStoreNative = new DebugStoreNativeImpl();
+
+    @UnsupportedAppUsage
+    @VisibleForTesting
+    public static void setDebugStoreNative(DebugStoreNative nativeImpl) {
+        sDebugStoreNative = nativeImpl;
+    }
+    /**
+     * Records the start of a service.
+     *
+     * @param startId The start ID of the service.
+     * @param flags Additional flags for the service start.
+     * @param intent The Intent associated with the service start.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceOnStart(int startId, int flags, @Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "SvcStart",
+                List.of(
+                        "stId",
+                        String.valueOf(startId),
+                        "flg",
+                        Integer.toHexString(flags),
+                        "act",
+                        Objects.toString(intent != null ? intent.getAction() : null),
+                        "comp",
+                        Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg",
+                        Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Records the creation of a service.
+     *
+     * @param serviceInfo Information about the service being created.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceCreate(@Nullable ServiceInfo serviceInfo) {
+        return sDebugStoreNative.beginEvent(
+                "SvcCreate",
+                List.of(
+                        "name",
+                        Objects.toString(serviceInfo != null ? serviceInfo.name : null),
+                        "pkg",
+                        Objects.toString(serviceInfo != null ? serviceInfo.packageName : null)));
+    }
+
+    /**
+     * Records the binding of a service.
+     *
+     * @param isRebind Indicates whether the service is being rebound.
+     * @param intent The Intent associated with the service binding.
+     * @return A unique identifier for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceBind(boolean isRebind, @Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "SvcBind",
+                List.of(
+                        "rebind",
+                        String.valueOf(isRebind),
+                        "act",
+                        Objects.toString(intent != null ? intent.getAction() : null),
+                        "cmp",
+                        Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg",
+                        Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Records an asynchronous operation initiated by a broadcast receiver through calling GoAsync.
+     *
+     * @param receiverClassName The class name of the broadcast receiver.
+     */
+    @UnsupportedAppUsage
+    public static void recordGoAsync(String receiverClassName) {
+        sDebugStoreNative.recordEvent(
+                "GoAsync",
+                List.of(
+                        "tname",
+                        Thread.currentThread().getName(),
+                        "tid",
+                        String.valueOf(Thread.currentThread().getId()),
+                        "rcv",
+                        Objects.toString(receiverClassName)));
+    }
+
+    /**
+     * Records the completion of a broadcast operation through calling Finish.
+     *
+     * @param receiverClassName The class of the broadcast receiver that completed the operation.
+     */
+    @UnsupportedAppUsage
+    public static void recordFinish(String receiverClassName) {
+        sDebugStoreNative.recordEvent(
+                "Finish",
+                List.of(
+                        "tname",
+                        Thread.currentThread().getName(),
+                        "tid",
+                        String.valueOf(Thread.currentThread().getId()),
+                        "rcv",
+                        Objects.toString(receiverClassName)));
+    }
+    /**
+     * Records the completion of a long-running looper message.
+     *
+     * @param messageCode The code representing the type of the message.
+     * @param targetClass The FQN of the class that handled the message.
+     * @param elapsedTimeMs The time that was taken to process the message, in milliseconds.
+     */
+    @UnsupportedAppUsage
+    public static void recordLongLooperMessage(int messageCode, String targetClass,
+            long elapsedTimeMs) {
+        sDebugStoreNative.recordEvent(
+                "LooperMsg",
+                List.of(
+                        "code",
+                        String.valueOf(messageCode),
+                        "trgt",
+                        Objects.toString(targetClass),
+                        "elapsed",
+                        String.valueOf(elapsedTimeMs)));
+    }
+
+
+    /**
+     * Records the reception of a broadcast.
+     *
+     * @param intent The Intent associated with the broadcast.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordBroadcastHandleReceiver(@Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "HandleReceiver",
+                List.of(
+                        "tname", Thread.currentThread().getName(),
+                        "tid", String.valueOf(Thread.currentThread().getId()),
+                        "act", Objects.toString(intent != null ? intent.getAction() : null),
+                        "cmp", Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg", Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Ends a previously recorded event.
+     *
+     * @param id The unique ID of the event to be ended.
+     */
+    @UnsupportedAppUsage
+    public static void recordEventEnd(long id) {
+        sDebugStoreNative.endEvent(id, Collections.emptyList());
+    }
+
+    /**
+     * An interface for a class that acts as a wrapper for the static native methods
+     * of the Debug Store.
+     *
+     * It allows us to mock static native methods in our tests and should be removed
+     * once mocking static methods becomes easier.
+     */
+    @VisibleForTesting
+    public interface DebugStoreNative {
+        /**
+         * Begins an event with the given name and attributes.
+         */
+        long beginEvent(String eventName, List<String> attributes);
+        /**
+         * Ends an event with the given ID and attributes.
+         */
+        void endEvent(long id, List<String> attributes);
+        /**
+         * Records an event with the given name and attributes.
+         */
+        void recordEvent(String eventName, List<String> attributes);
+    }
+
+    private static class DebugStoreNativeImpl implements DebugStoreNative {
+        @Override
+        public long beginEvent(String eventName, List<String> attributes) {
+            return DebugStore.beginEventNative(eventName, attributes);
+        }
+
+        @Override
+        public void endEvent(long id, List<String> attributes) {
+            DebugStore.endEventNative(id, attributes);
+        }
+
+        @Override
+        public void recordEvent(String eventName, List<String> attributes) {
+            DebugStore.recordEventNative(eventName, attributes);
+        }
+    }
+
+    private static native long beginEventNative(String eventName, List<String> attributes);
+
+    private static native void endEventNative(long id, List<String> attributes);
+
+    private static native void recordEventNative(String eventName, List<String> attributes);
+}
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 2ad6651..c7117e9 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -19,4 +19,12 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "debug_store_enabled"
+    namespace: "stability"
+    description: "If the debug store is enabled."
+    bug: "314735374"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java
index 77de272..0fd1391 100644
--- a/core/java/com/android/internal/util/ContrastColorUtil.java
+++ b/core/java/com/android/internal/util/ContrastColorUtil.java
@@ -31,6 +31,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.graphics.drawable.VectorDrawable;
+import android.text.NoCopySpan;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.BackgroundColorSpan;
@@ -188,6 +189,10 @@
             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
             for (Object span : spans) {
+                if (span instanceof NoCopySpan) {
+                    // These spans can contain external references and should not be copied.
+                    continue;
+                }
                 Object resultSpan = span;
                 if (resultSpan instanceof CharacterStyle) {
                     resultSpan = ((CharacterStyle) span).getUnderlying();
@@ -254,6 +259,10 @@
             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
             for (Object span : spans) {
+                if (span instanceof NoCopySpan) {
+                    // These spans can contain external references and should not be copied.
+                    continue;
+                }
                 Object resultSpan = span;
                 if (resultSpan instanceof CharacterStyle) {
                     resultSpan = ((CharacterStyle) span).getUnderlying();
@@ -300,6 +309,10 @@
             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
             for (Object span : spans) {
+                if (span instanceof NoCopySpan) {
+                    // These spans can contain external references and should not be copied.
+                    continue;
+                }
                 Object resultSpan = span;
                 int spanStart = ss.getSpanStart(span);
                 int spanEnd = ss.getSpanEnd(span);
diff --git a/core/java/com/android/internal/widget/OWNERS b/core/java/com/android/internal/widget/OWNERS
index cf2f202..2d1c2f0 100644
--- a/core/java/com/android/internal/widget/OWNERS
+++ b/core/java/com/android/internal/widget/OWNERS
@@ -3,7 +3,9 @@
 per-file ViewPager.java = mount@google.com
 
 # LockSettings related
-per-file *LockPattern* = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternChecker.java = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternUtils.java = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternView.java = file:/packages/SystemUI/OWNERS
 per-file *LockScreen* = file:/services/core/java/com/android/server/locksettings/OWNERS
 per-file *Lockscreen* = file:/services/core/java/com/android/server/locksettings/OWNERS
 per-file *LockSettings* = file:/services/core/java/com/android/server/locksettings/OWNERS
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 5c2a167..effbbe2 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -18,6 +18,11 @@
 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -30,6 +35,9 @@
 public class CoreDocument {
 
     ArrayList<Operation> mOperations;
+
+    RootLayoutComponent mRootLayoutComponent = null;
+
     RemoteComposeState mRemoteComposeState = new RemoteComposeState();
     TimeVariables mTimeVariables = new TimeVariables();
     // Semantic version of the document
@@ -81,7 +89,6 @@
     public void setHeight(int height) {
         this.mHeight = height;
         mRemoteComposeState.setWindowHeight(height);
-
     }
 
     public RemoteComposeBuffer getBuffer() {
@@ -259,10 +266,43 @@
         translateOutput[1] = translateY;
     }
 
+    /**
+     * Returns the list of click areas
+     * @return list of click areas in document coordinates
+     */
     public Set<ClickAreaRepresentation> getClickAreas() {
         return mClickAreas;
     }
 
+    /**
+     * Returns the root layout component
+     * @return returns the root component if it exists, null otherwise
+     */
+    public RootLayoutComponent getRootLayoutComponent() {
+        return mRootLayoutComponent;
+    }
+
+    /**
+     * Invalidate the document for layout measures. This will trigger a layout remeasure pass.
+     */
+    public void invalidateMeasure() {
+        if (mRootLayoutComponent != null) {
+            mRootLayoutComponent.invalidateMeasure();
+        }
+    }
+
+    /**
+     * Returns the component with the given id
+     * @param id component id
+     * @return the component if it exists, null otherwise
+     */
+    public Component getComponent(int id) {
+        if (mRootLayoutComponent != null) {
+            return mRootLayoutComponent.getComponent(id);
+        }
+        return null;
+    }
+
     public interface ClickCallbacks {
         void click(int id, String metadata);
     }
@@ -354,7 +394,54 @@
     public void initFromBuffer(RemoteComposeBuffer buffer) {
         mOperations = new ArrayList<Operation>();
         buffer.inflateFromBuffer(mOperations);
+        mOperations = inflateComponents(mOperations);
         mBuffer = buffer;
+        for (Operation op : mOperations) {
+            if (op instanceof RootLayoutComponent) {
+                mRootLayoutComponent = (RootLayoutComponent) op;
+                break;
+            }
+        }
+        if (mRootLayoutComponent != null) {
+            mRootLayoutComponent.assignIds();
+        }
+    }
+
+    /**
+     * Inflate a component tree
+     * @param operations flat list of operations
+     * @return nested list of operations / components
+     */
+    private ArrayList<Operation> inflateComponents(ArrayList<Operation> operations) {
+        Component currentComponent = null;
+        ArrayList<Component> components = new ArrayList<>();
+        ArrayList<Operation> finalOperationsList = new ArrayList<>();
+        ArrayList<Operation> ops = finalOperationsList;
+
+        for (Operation o : operations) {
+            if (o instanceof ComponentStartOperation) {
+                Component component = (Component) o;
+                component.setParent(currentComponent);
+                components.add(component);
+                currentComponent = component;
+                ops.add(currentComponent);
+                ops = currentComponent.getList();
+            } else if (o instanceof ComponentEnd) {
+                if (currentComponent instanceof LayoutComponent) {
+                    ((LayoutComponent) currentComponent).inflate();
+                }
+                components.remove(components.size() - 1);
+                if (!components.isEmpty()) {
+                    currentComponent = components.get(components.size() - 1);
+                    ops = currentComponent.getList();
+                } else {
+                    ops = finalOperationsList;
+                }
+            } else {
+                ops.add(o);
+            }
+        }
+        return ops;
     }
 
     /**
@@ -559,6 +646,18 @@
         context.loadFloat(RemoteContext.ID_WINDOW_WIDTH, getWidth());
         context.loadFloat(RemoteContext.ID_WINDOW_HEIGHT, getHeight());
         mRepaintNext = context.updateOps();
+        if (mRootLayoutComponent != null) {
+            if (context.mWidth != mRootLayoutComponent.getWidth()
+                    || context.mHeight != mRootLayoutComponent.getHeight()) {
+                mRootLayoutComponent.invalidateMeasure();
+            }
+            if (mRootLayoutComponent.needsMeasure()) {
+                mRootLayoutComponent.layout(context);
+            }
+            if (mRootLayoutComponent.doesNeedsRepaint()) {
+                mRepaintNext = 1;
+            }
+        }
         for (Operation op : mOperations) {
             // operations will only be executed if no theme is set (ie UNSPECIFIED)
             // or the theme is equal as the one passed in argument to paint.
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
index 7cb9a42..4a8b3d7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
@@ -37,4 +37,3 @@
      */
     String deepToString(String indent);
 }
-
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index 4b8dbf6..9cb024b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -54,6 +54,21 @@
 import com.android.internal.widget.remotecompose.core.operations.TextFromFloat;
 import com.android.internal.widget.remotecompose.core.operations.TextMerge;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponentContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.BoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BackgroundModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
 import com.android.internal.widget.remotecompose.core.types.BooleanConstant;
 import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
@@ -117,6 +132,27 @@
     public static final int INTEGER_EXPRESSION = 144;
 
     /////////////////////////////////////////======================
+
+    ////////////////////////////////////////
+    // Layout commands
+    ////////////////////////////////////////
+
+    public static final int LAYOUT_ROOT = 200;
+    public static final int LAYOUT_CONTENT = 201;
+    public static final int LAYOUT_BOX = 202;
+    public static final int LAYOUT_ROW = 203;
+    public static final int LAYOUT_COLUMN = 204;
+    public static final int COMPONENT_START = 2;
+    public static final int COMPONENT_END = 3;
+    public static final int MODIFIER_WIDTH = 16;
+    public static final int MODIFIER_HEIGHT = 67;
+    public static final int MODIFIER_BACKGROUND = 55;
+    public static final int MODIFIER_BORDER = 107;
+    public static final int MODIFIER_PADDING = 58;
+    public static final int MODIFIER_CLIP_RECT = 108;
+    public static final int MODIFIER_ROUNDED_CLIP_RECT = 54;
+    public static final int ANIMATION_SPEC = 14;
+
     public static IntMap<CompanionOperation> map = new IntMap<>();
 
     static {
@@ -162,6 +198,26 @@
         map.put(DATA_INT, IntegerConstant.COMPANION);
         map.put(INTEGER_EXPRESSION, IntegerExpression.COMPANION);
         map.put(DATA_BOOLEAN, BooleanConstant.COMPANION);
+
+        // Layout
+
+        map.put(COMPONENT_START, ComponentStart.COMPANION);
+        map.put(COMPONENT_END, ComponentEnd.COMPANION);
+        map.put(ANIMATION_SPEC, AnimationSpec.COMPANION);
+
+        map.put(MODIFIER_WIDTH, WidthModifierOperation.COMPANION);
+        map.put(MODIFIER_HEIGHT, HeightModifierOperation.COMPANION);
+        map.put(MODIFIER_PADDING, PaddingModifierOperation.COMPANION);
+        map.put(MODIFIER_BACKGROUND, BackgroundModifierOperation.COMPANION);
+        map.put(MODIFIER_BORDER, BorderModifierOperation.COMPANION);
+        map.put(MODIFIER_ROUNDED_CLIP_RECT, RoundedClipRectModifierOperation.COMPANION);
+        map.put(MODIFIER_CLIP_RECT, ClipRectModifierOperation.COMPANION);
+
+        map.put(LAYOUT_ROOT, RootLayoutComponent.COMPANION);
+        map.put(LAYOUT_CONTENT, LayoutComponentContent.COMPANION);
+        map.put(LAYOUT_BOX, BoxLayout.COMPANION);
+        map.put(LAYOUT_COLUMN, ColumnLayout.COMPANION);
+        map.put(LAYOUT_ROW, RowLayout.COMPANION);
     }
 
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index 6d8a442..665fcb7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -23,6 +23,10 @@
 public abstract class PaintContext {
     protected RemoteContext mContext;
 
+    public RemoteContext getContext() {
+        return mContext;
+    }
+
     public PaintContext(RemoteContext context) {
         this.mContext = context;
     }
@@ -31,6 +35,28 @@
         this.mContext = context;
     }
 
+    /**
+     * convenience function to call matrixSave()
+     */
+    public void save() {
+        matrixSave();
+    }
+
+    /**
+     * convenience function to call matrixRestore()
+     */
+    public void restore() {
+        matrixRestore();
+    }
+
+    /**
+     * convenience function to call matrixSave()
+     */
+    public void saveLayer(float x, float y, float width, float height) {
+        // TODO
+        matrixSave();
+    }
+
     public abstract void drawBitmap(int imageId,
                                     int srcLeft, int srcTop, int srcRight, int srcBottom,
                                     int dstLeft, int dstTop, int dstRight, int dstBottom,
@@ -197,8 +223,49 @@
     public abstract void clipPath(int pathId, int regionOp);
 
     /**
+     * Clip based ona  round rect
+     * @param width
+     * @param height
+     * @param topStart
+     * @param topEnd
+     * @param bottomStart
+     * @param bottomEnd
+     */
+    public abstract void roundedClipRect(float width, float height,
+                                         float topStart, float topEnd,
+                                         float bottomStart, float bottomEnd);
+
+    /**
      * Reset the paint
      */
     public abstract void reset();
+
+    /**
+     * Returns true if the context is in debug mode
+     *
+     * @return true if in debug mode, false otherwise
+     */
+    public boolean isDebug() {
+        return mContext.isDebug();
+    }
+
+    /**
+     * Returns true if layout animations are enabled
+     *
+     * @return true if animations are enabled, false otherwise
+     */
+    public boolean isAnimationEnabled() {
+        return mContext.isAnimationEnabled();
+    }
+
+    /**
+     * Utility function to log comments
+     *
+     * @param content the content to log
+     */
+    public void log(String content) {
+        System.out.println("[LOG] " + content);
+    }
+
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
index 2f3fe57..4a1ccc9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
@@ -23,9 +23,11 @@
 
     @Override
     public void apply(RemoteContext context) {
-        if (context.getMode() == RemoteContext.ContextMode.PAINT
-                && context.getPaintContext() != null) {
-            paint((PaintContext) context.getPaintContext());
+        if (context.getMode() == RemoteContext.ContextMode.PAINT) {
+            PaintContext paintContext = context.getPaintContext();
+            if (paintContext != null) {
+                paint(paintContext);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index f5f155e..333951b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -54,6 +54,18 @@
 import com.android.internal.widget.remotecompose.core.operations.TextMerge;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponentContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.BoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BackgroundModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
 import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
@@ -132,8 +144,9 @@
      * @param contentDescription content description of the document
      * @param capabilities       bitmask indicating needed capabilities (unused for now)
      */
-    public void header(int width, int height, String contentDescription, long capabilities) {
-        Header.COMPANION.apply(mBuffer, width, height, capabilities);
+    public void header(int width, int height, String contentDescription,
+                       float density, long capabilities) {
+        Header.COMPANION.apply(mBuffer, width, height, density, capabilities);
         int contentDescriptionId = 0;
         if (contentDescription != null) {
             contentDescriptionId = addText(contentDescription);
@@ -149,7 +162,7 @@
      * @param contentDescription content description of the document
      */
     public void header(int width, int height, String contentDescription) {
-        header(width, height, contentDescription, 0);
+        header(width, height, contentDescription, 1f, 0);
     }
 
     /**
@@ -857,7 +870,7 @@
     }
 
     /**
-     * Sets the clip based on clip rec
+     * Sets the clip based on clip rect
      * @param left
      * @param top
      * @param right
@@ -1074,5 +1087,128 @@
                 NamedVariable.COLOR_TYPE, name);
     }
 
+    /**
+     * Add a component start tag
+     * @param type type of component
+     * @param id component id
+     */
+    public void addComponentStart(int type, int id) {
+        switch (type) {
+            case ComponentStart.ROOT_LAYOUT: {
+                RootLayoutComponent.COMPANION.apply(mBuffer);
+            } break;
+            case ComponentStart.LAYOUT_CONTENT: {
+                LayoutComponentContent.COMPANION.apply(mBuffer);
+            } break;
+            case ComponentStart.LAYOUT_BOX: {
+                BoxLayout.COMPANION.apply(mBuffer, id, -1,
+                        BoxLayout.CENTER, BoxLayout.CENTER);
+            } break;
+            case ComponentStart.LAYOUT_ROW: {
+                RowLayout.COMPANION.apply(mBuffer, id, -1,
+                        RowLayout.START, RowLayout.TOP, 0f);
+            } break;
+            case ComponentStart.LAYOUT_COLUMN: {
+                ColumnLayout.COMPANION.apply(mBuffer, id, -1,
+                        ColumnLayout.START, ColumnLayout.TOP, 0f);
+            } break;
+            default:
+                ComponentStart.Companion.apply(mBuffer,
+                        type, id, 0f, 0f);
+        }
+    }
+
+    /**
+     * Add a component start tag
+     * @param type type of component
+     */
+    public void addComponentStart(int type) {
+        addComponentStart(type, -1);
+    }
+
+    /**
+     * Add a component end tag
+     */
+    public void addComponentEnd() {
+        ComponentEnd.Companion.apply(mBuffer);
+    }
+
+    /**
+     * Add a background modifier of provided color
+     * @param color the color of the background
+     * @param shape the background shape -- SHAPE_RECTANGLE, SHAPE_CIRCLE
+     */
+    public void addModifierBackground(int color, int shape) {
+        float r = ((color >> 16) & 0xff) / 255.0f;
+        float g = ((color >> 8) & 0xff) / 255.0f;
+        float b = ((color) & 0xff) / 255.0f;
+        float a = ((color >> 24) & 0xff) / 255.0f;
+        BackgroundModifierOperation.COMPANION.apply(mBuffer, 0f, 0f, 0f, 0f,
+                r, g, b, a, shape);
+    }
+
+    /**
+     * Add a border modifier
+     * @param borderWidth the border width
+     * @param borderRoundedCorner the rounded corner radius if the shape is ROUNDED_RECT
+     * @param color the color of the border
+     * @param shape the shape of the border
+     */
+    public void addModifierBorder(float borderWidth, float borderRoundedCorner,
+                                  int color, int shape) {
+        float r = ((color >> 16) & 0xff) / 255.0f;
+        float g = ((color >>  8) & 0xff) / 255.0f;
+        float b = ((color) & 0xff) / 255.0f;
+        float a = ((color >> 24) & 0xff) / 255.0f;
+        BorderModifierOperation.COMPANION.apply(mBuffer, 0f, 0f, 0f, 0f,
+                borderWidth, borderRoundedCorner, r, g, b, a, shape);
+    }
+
+    /**
+     * Add a padding modifier
+     * @param left left padding
+     * @param top top padding
+     * @param right right padding
+     * @param bottom bottom padding
+     */
+    public void addModifierPadding(float left, float top, float right, float bottom) {
+        PaddingModifierOperation.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+
+    /**
+     * Sets the clip based on rounded clip rect
+     * @param topStart
+     * @param topEnd
+     * @param bottomStart
+     * @param bottomEnd
+     */
+    public void addRoundClipRectModifier(float topStart, float topEnd,
+                                         float bottomStart, float bottomEnd) {
+        RoundedClipRectModifierOperation.COMPANION.apply(mBuffer,
+                topStart, topEnd, bottomStart, bottomEnd);
+    }
+
+    public void addClipRectModifier() {
+        ClipRectModifierOperation.COMPANION.apply(mBuffer);
+    }
+
+    public void addBoxStart(int componentId, int animationId,
+                            int horizontal, int vertical) {
+        BoxLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical);
+    }
+
+    public void addRowStart(int componentId, int animationId,
+                            int horizontal, int vertical, float spacedBy) {
+        RowLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical, spacedBy);
+    }
+
+    public void addColumnStart(int componentId, int animationId,
+                            int horizontal, int vertical, float spacedBy) {
+        ColumnLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical, spacedBy);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 41eeb5b..893dcce 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -19,6 +19,7 @@
 import com.android.internal.widget.remotecompose.core.operations.ShaderData;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
 
 /**
  * Specify an abstract context used to playback RemoteCompose documents
@@ -35,12 +36,26 @@
     ContextMode mMode = ContextMode.UNSET;
 
     boolean mDebug = false;
+
     private int mTheme = Theme.UNSPECIFIED;
 
     public float mWidth = 0f;
     public float mHeight = 0f;
     private float mAnimationTime;
 
+    private boolean mAnimate = true;
+
+    public Component lastComponent;
+    public long currentTime = 0L;
+
+    public boolean isAnimationEnabled() {
+        return mAnimate;
+    }
+
+    public void setAnimationEnabled(boolean value) {
+        mAnimate = value;
+    }
+
     /**
      * Load a path under an id.
      * Paths can be use in clip drawPath and drawTweenPath
@@ -333,9 +348,11 @@
     public static final float FLOAT_COMPONENT_HEIGHT = Utils.asNan(ID_COMPONENT_HEIGHT);
     // ID_OFFSET_TO_UTC is the offset from UTC in sec (typically / 3600f)
     public static final float FLOAT_OFFSET_TO_UTC = Utils.asNan(ID_OFFSET_TO_UTC);
+
     ///////////////////////////////////////////////////////////////////////////////////////////////
     // Click handling
     ///////////////////////////////////////////////////////////////////////////////////////////////
+
     public abstract void addClickArea(
             int id,
             int contentDescription,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
similarity index 63%
copy from packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
copy to core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
index 9a5c77a..ccbcdf6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -13,9 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.internal.widget.remotecompose.core.documentation;
 
-package com.android.systemui.shared.education
-
-enum class GestureType {
-    BACK_GESTURE,
+public interface DocumentationBuilder {
+    void add(String value);
+    Operation operation(String category, int id, String name);
+    Operation wipOperation(String category, int id, String name);
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
similarity index 63%
copy from packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
copy to core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
index 9a5c77a..6a98b78 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -13,9 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.internal.widget.remotecompose.core.documentation;
 
-package com.android.systemui.shared.education
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
 
-enum class GestureType {
-    BACK_GESTURE,
+public interface DocumentedCompanionOperation extends CompanionOperation {
+    void documentation(DocumentationBuilder doc);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java
new file mode 100644
index 0000000..643b925
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java
@@ -0,0 +1,151 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+import java.util.ArrayList;
+
+public class Operation {
+    public static final int LAYOUT = 0;
+    public static final int INT = 0;
+    public static final int FLOAT = 1;
+    public static final int BOOLEAN = 2;
+    public static final int BUFFER = 4;
+    public static final int UTF8 = 5;
+    public static final int BYTE = 6;
+    public static final int VALUE = 7;
+    public static final int LONG = 8;
+
+    String mCategory;
+    int mId;
+    String mName;
+    String mDescription;
+
+    boolean mWIP;
+    String mTextExamples;
+
+    ArrayList<StringPair> mExamples = new ArrayList<>();
+    ArrayList<OperationField> mFields = new ArrayList<>();
+
+    int mExamplesWidth = 100;
+    int mExamplesHeight = 100;
+
+
+    public static String getType(int type) {
+        switch (type) {
+            case (INT): return "INT";
+            case (FLOAT): return "FLOAT";
+            case (BOOLEAN): return "BOOLEAN";
+            case (BUFFER): return "BUFFER";
+            case (UTF8): return "UTF8";
+            case (BYTE): return "BYTE";
+            case (VALUE): return "VALUE";
+            case (LONG): return "LONG";
+        }
+        return "UNKNOWN";
+    }
+
+    public Operation(String category, int id, String name, boolean wip) {
+        mCategory = category;
+        mId = id;
+        mName = name;
+        mWIP = wip;
+    }
+
+    public Operation(String category, int id, String name) {
+        this(category, id, name, false);
+    }
+
+    public ArrayList<OperationField> getFields() {
+        return mFields;
+    }
+
+    public String getCategory() {
+        return mCategory;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public boolean isWIP() {
+        return mWIP;
+    }
+
+    public int getSizeFields() {
+        int size = 0;
+        for (OperationField field : mFields) {
+            size += field.getSize();
+        }
+        return size;
+    }
+
+    public String getDescription() {
+        return mDescription;
+    }
+
+    public String getTextExamples() {
+        return mTextExamples;
+    }
+
+    public ArrayList<StringPair> getExamples() {
+        return mExamples;
+    }
+
+    public int getExamplesWidth() {
+        return mExamplesWidth;
+    }
+
+    public int getExamplesHeight() {
+        return mExamplesHeight;
+    }
+
+    public Operation field(int type, String name, String description) {
+        mFields.add(new OperationField(type, name, description));
+        return this;
+    }
+
+    public Operation possibleValues(String name, int value) {
+        if (!mFields.isEmpty()) {
+            mFields.get(mFields.size() - 1).possibleValue(name, "" + value);
+        }
+        return this;
+    }
+
+    public Operation description(String description) {
+        mDescription = description;
+        return this;
+    }
+
+    public Operation examples(String examples) {
+        mTextExamples = examples;
+        return this;
+    }
+
+    public Operation exampleImage(String name, String imagePath) {
+        mExamples.add(new StringPair(name, imagePath));
+        return this;
+    }
+
+    public Operation examplesDimension(int width, int height) {
+        mExamplesWidth = width;
+        mExamplesHeight = height;
+        return this;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
new file mode 100644
index 0000000..fc73f4ed6
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
@@ -0,0 +1,58 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+import java.util.ArrayList;
+
+public class OperationField {
+    int mType;
+    String mName;
+    String mDescription;
+    ArrayList<StringPair> mPossibleValues = new ArrayList<>();
+
+    public OperationField(int type, String name, String description) {
+        mType = type;
+        mName = name;
+        mDescription = description;
+    }
+    public int getType() {
+        return mType;
+    }
+    public String getName() {
+        return mName;
+    }
+    public String getDescription() {
+        return mDescription;
+    }
+    public ArrayList<StringPair> getPossibleValues() {
+        return mPossibleValues;
+    }
+    public void possibleValue(String name, String value) {
+        mPossibleValues.add(new StringPair(name, value));
+    }
+    public boolean hasEnumeratedValues() {
+        return !mPossibleValues.isEmpty();
+    }
+    public int getSize() {
+        switch (mType) {
+            case (Operation.BYTE) : return 1;
+            case (Operation.INT) : return 4;
+            case (Operation.FLOAT) : return 4;
+            case (Operation.LONG) : return 8;
+            default : return 0;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
similarity index 60%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
copy to core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
index 14b9e7f..787bb54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
@@ -13,15 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.wm.shell.windowdecor.common
+package com.android.internal.widget.remotecompose.core.documentation;
+public class StringPair {
+    String mName;
+    String mValue;
 
-/** A callback to be invoked when a Task's window decor element is clicked. */
-fun interface OnTaskActionClickListener {
-    /**
-     * Called when a task's decor element has been clicked.
-     *
-     * @param taskId the id of the task.
-     * @param tag a readable identifier for the element.
-     */
-    fun onClick(taskId: Int, tag: String)
+    StringPair(String name, String value) {
+        mName = name;
+        mValue = value;
+    }
+
+    public String getName() {
+        return mName;
+    }
+    public String getValue() {
+        return mValue;
+    }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
index ec35a16..53a3aa9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
@@ -41,10 +41,10 @@
                 }
             };
     protected String mName = "DrawRectBase";
-    float mX1;
-    float mY1;
-    float mX2;
-    float mY2;
+    protected float mX1;
+    protected float mY1;
+    protected float mX2;
+    protected float mY2;
     float mX1Value;
     float mY1Value;
     float mX2Value;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
index aabed15..9a1f37b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -15,12 +15,16 @@
  */
 package com.android.internal.widget.remotecompose.core.operations;
 
-import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.LONG;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
 
 import java.util.List;
 
@@ -41,6 +45,8 @@
 
     int mWidth;
     int mHeight;
+
+    float mDensity;
     long mCapabilities;
 
     public static final Companion COMPANION = new Companion();
@@ -54,21 +60,23 @@
      * @param patchVersion the patch version of the RemoteCompose document API
      * @param width        the width of the RemoteCompose document
      * @param height       the height of the RemoteCompose document
+     * @param density      the density at which the document was originally created
      * @param capabilities bitmask field storing needed capabilities (unused for now)
      */
     public Header(int majorVersion, int minorVersion, int patchVersion,
-                  int width, int height, long capabilities) {
+                  int width, int height, float density, long capabilities) {
         this.mMajorVersion = majorVersion;
         this.mMinorVersion = minorVersion;
         this.mPatchVersion = patchVersion;
         this.mWidth = width;
         this.mHeight = height;
+        this.mDensity = density;
         this.mCapabilities = capabilities;
     }
 
     @Override
     public void write(WireBuffer buffer) {
-        COMPANION.apply(buffer, mWidth, mHeight, mCapabilities);
+        COMPANION.apply(buffer, mWidth, mHeight, mDensity, mCapabilities);
     }
 
     @Override
@@ -88,7 +96,7 @@
         return toString();
     }
 
-    public static class Companion implements CompanionOperation {
+    public static class Companion implements DocumentedCompanionOperation {
         private Companion() {
         }
 
@@ -102,13 +110,15 @@
             return Operations.HEADER;
         }
 
-        public void apply(WireBuffer buffer, int width, int height, long capabilities) {
+        public void apply(WireBuffer buffer, int width, int height,
+                          float density, long capabilities) {
             buffer.start(Operations.HEADER);
             buffer.writeInt(MAJOR_VERSION); // major version number of the protocol
             buffer.writeInt(MINOR_VERSION); // minor version number of the protocol
             buffer.writeInt(PATCH_VERSION); // patch version number of the protocol
             buffer.writeInt(width);
             buffer.writeInt(height);
+            // buffer.writeFloat(density);
             buffer.writeLong(capabilities);
         }
 
@@ -119,10 +129,26 @@
             int patchVersion = buffer.readInt();
             int width = buffer.readInt();
             int height = buffer.readInt();
+            // float density = buffer.readFloat();
+            float density = 1f;
             long capabilities = buffer.readLong();
             Header header = new Header(majorVersion, minorVersion, patchVersion,
-                    width, height, capabilities);
+                    width, height, density, capabilities);
             operations.add(header);
         }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Protocol Operations", id(), name())
+                    .description("Document metadata, containing the version,"
+                          + " original size & density, capabilities mask")
+                    .field(INT, "MAJOR_VERSION", "Major version")
+                    .field(INT, "MINOR_VERSION", "Minor version")
+                    .field(INT, "PATCH_VERSION", "Patch version")
+                    .field(INT, "WIDTH", "Major version")
+                    .field(INT, "HEIGHT", "Major version")
+                    // .field(FLOAT, "DENSITY", "Major version")
+                    .field(LONG, "CAPABILITIES", "Major version");
+        }
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
index cbe9c12..f982997 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
@@ -15,12 +15,15 @@
  */
 package com.android.internal.widget.remotecompose.core.operations;
 
-import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
 
 import java.util.List;
 
@@ -70,12 +73,12 @@
         return indent + toString();
     }
 
-    public static class Companion implements CompanionOperation {
+    public static class Companion implements DocumentedCompanionOperation {
         private Companion() {}
 
         @Override
         public String name() {
-            return "SetTheme";
+            return "Theme";
         }
 
         @Override
@@ -93,5 +96,15 @@
             int theme = buffer.readInt();
             operations.add(new Theme(theme));
         }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Protocol Operations", id(), name())
+                    .description("Set a theme")
+                    .field(INT, "THEME", "theme id")
+                    .possibleValues("UNSPECIFIED", Theme.UNSPECIFIED)
+                    .possibleValues("DARK", Theme.DARK)
+                    .possibleValues("LIGHT", Theme.LIGHT);
+        }
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
new file mode 100644
index 0000000..ee2e11b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
@@ -0,0 +1,473 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimateMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.ArrayList;
+
+/**
+ * Generic Component class
+ */
+public class Component extends PaintOperation implements Measurable {
+
+    protected int mComponentId = -1;
+    protected float mX;
+    protected float mY;
+    protected float mWidth;
+    protected float mHeight;
+    protected Component mParent;
+    protected int mAnimationId = -1;
+    public Visibility mVisibility = Visibility.VISIBLE;
+    public ArrayList<Operation> mList = new ArrayList<>();
+    public PaintOperation mPreTranslate;
+    public boolean mNeedsMeasure = true;
+    public boolean mNeedsRepaint = false;
+    public AnimateMeasure mAnimateMeasure;
+    public AnimationSpec mAnimationSpec = new AnimationSpec();
+    public boolean mFirstLayout = true;
+    PaintBundle mPaint = new PaintBundle();
+
+    public ArrayList<Operation> getList() {
+        return mList;
+    }
+    public float getX() {
+        return mX;
+    }
+    public float getY() {
+        return mY;
+    }
+    public float getWidth() {
+        return mWidth;
+    }
+    public float getHeight() {
+        return mHeight;
+    }
+    public int getComponentId() {
+        return mComponentId;
+    }
+
+    public int getAnimationId() {
+        return mAnimationId;
+    }
+
+    public Component getParent() {
+        return mParent;
+    }
+    public void setX(float value) {
+        mX = value;
+    }
+    public void setY(float value) {
+        mY = value;
+    }
+    public void setWidth(float value) {
+        mWidth = value;
+    }
+    public void setHeight(float value) {
+        mHeight = value;
+    }
+
+    public void setComponentId(int id) {
+        mComponentId = id;
+    }
+
+    public void setAnimationId(int id) {
+        mAnimationId = id;
+    }
+
+    public Component(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height) {
+        this.mComponentId = componentId;
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mParent = parent;
+        this.mAnimationId = animationId;
+    }
+
+    public Component(int componentId, float x, float y, float width, float height,
+                     Component parent) {
+        this(parent, componentId, -1, x, y, width, height);
+    }
+
+    public Component(Component component) {
+        this(component.mParent, component.mComponentId, component.mAnimationId,
+                component.mX, component.mY, component.mWidth, component.mHeight
+        );
+        mList.addAll(component.mList);
+        finalizeCreation();
+    }
+
+    public void finalizeCreation() {
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                ((Component) op).mParent = this;
+            }
+            if (op instanceof AnimationSpec) {
+                mAnimationSpec = (AnimationSpec) op;
+                mAnimationId = mAnimationSpec.getAnimationId();
+            }
+        }
+    }
+
+    @Override
+    public boolean needsMeasure() {
+        return mNeedsMeasure;
+    }
+
+    public void setParent(Component parent) {
+        mParent = parent;
+    }
+
+    public enum Visibility {
+        VISIBLE,
+        INVISIBLE,
+        GONE
+    }
+
+    public boolean isVisible() {
+        if (mVisibility != Visibility.VISIBLE || mParent == null) {
+            return mVisibility == Visibility.VISIBLE;
+        }
+        if (mParent != null) {
+            return mParent.isVisible();
+        }
+        return true;
+    }
+
+    @Override
+    public void measure(PaintContext context, float minWidth, float maxWidth,
+                        float minHeight, float maxHeight, MeasurePass measure) {
+        ComponentMeasure m = measure.get(this);
+        m.setW(mWidth);
+        m.setH(mHeight);
+    }
+
+    @Override
+    public void layout(RemoteContext context, MeasurePass measure) {
+        ComponentMeasure m = measure.get(this);
+        if (!mFirstLayout && context.isAnimationEnabled()) {
+            if (mAnimateMeasure == null) {
+                ComponentMeasure origin = new ComponentMeasure(mComponentId,
+                        mX, mY, mWidth, mHeight, mVisibility);
+                ComponentMeasure target = new ComponentMeasure(mComponentId,
+                        m.getX(), m.getY(), m.getW(), m.getH(), m.getVisibility());
+                mAnimateMeasure = new AnimateMeasure(context.currentTime, this,
+                        origin, target,
+                        mAnimationSpec.getMotionDuration(), mAnimationSpec.getVisibilityDuration(),
+                        mAnimationSpec.getEnterAnimation(), mAnimationSpec.getExitAnimation(),
+                        mAnimationSpec.getMotionEasingType(),
+                        mAnimationSpec.getVisibilityEasingType());
+            } else {
+                mAnimateMeasure.updateTarget(m, context.currentTime);
+            }
+        } else {
+            mVisibility = m.getVisibility();
+        }
+        mWidth = m.getW();
+        mHeight = m.getH();
+        setLayoutPosition(m.getX(), m.getY());
+        mFirstLayout = false;
+    }
+
+    public float[] locationInWindow = new float[2];
+
+    public boolean contains(float x, float y) {
+        locationInWindow[0] = 0f;
+        locationInWindow[1] = 0f;
+        getLocationInWindow(locationInWindow);
+        float lx1 = locationInWindow[0];
+        float lx2 = lx1 + mWidth;
+        float ly1 = locationInWindow[1];
+        float ly2 = ly1 + mHeight;
+        return x >= lx1 && x < lx2 && y >= ly1 && y < ly2;
+    }
+
+    public void onClick(float x, float y) {
+        if (!contains(x, y)) {
+            return;
+        }
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                ((Component) op).onClick(x, y);
+            }
+            if (op instanceof ComponentModifiers) {
+                ((ComponentModifiers) op).onClick(x, y);
+            }
+        }
+    }
+
+    public void getLocationInWindow(float[] value) {
+        value[0] += mX;
+        value[1] += mY;
+        if (mParent != null && mParent instanceof Component) {
+            if (mParent instanceof LayoutComponent) {
+                value[0] += ((LayoutComponent) mParent).getMarginLeft();
+                value[1] += ((LayoutComponent) mParent).getMarginTop();
+            }
+            mParent.getLocationInWindow(value);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT(<" + mComponentId + "> " + getClass().getSimpleName()
+                + ") [" + mX + "," + mY + " - " + mWidth + " x " + mHeight + "] " + textContent()
+                + " Visibility (" + mVisibility + ") ";
+    }
+
+    protected String getSerializedName() {
+        return "COMPONENT";
+    }
+
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, getSerializedName() + " [" + mComponentId
+                + ":" + mAnimationId + "] = "
+                + "[" + mX + ", " + mY + ", " + mWidth + ", " + mHeight + "] "
+                + mVisibility
+        //        + " [" + mNeedsMeasure + ", " + mNeedsRepaint + "]"
+        );
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        // nothing
+    }
+
+    /**
+     * Returns the top-level RootLayoutComponent
+     */
+    public RootLayoutComponent getRoot() throws Exception {
+        if (this instanceof RootLayoutComponent) {
+            return (RootLayoutComponent) this;
+        }
+        Component p = mParent;
+        while (!(p instanceof RootLayoutComponent)) {
+            if (p == null) {
+                throw new Exception("No RootLayoutComponent found");
+            }
+            p = p.mParent;
+        }
+        return (RootLayoutComponent) p;
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        StringBuilder builder = new StringBuilder();
+        builder.append(indent);
+        builder.append(toString());
+        builder.append("\n");
+        String indent2 = "  " + indent;
+        for (Operation op : mList) {
+            builder.append(op.deepToString(indent2));
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Mark itself as needing to be remeasured, and walk back up the tree
+     * to mark each parents as well.
+     */
+    public void invalidateMeasure() {
+        needsRepaint();
+        mNeedsMeasure = true;
+        Component p = mParent;
+        while (p != null) {
+            p.mNeedsMeasure = true;
+            p = p.mParent;
+        }
+    }
+
+    public void needsRepaint() {
+        try {
+            getRoot().mNeedsRepaint = true;
+        } catch (Exception e) {
+            // nothing
+        }
+    }
+
+    public String content() {
+        StringBuilder builder = new StringBuilder();
+        for (Operation op : mList) {
+            builder.append("- ");
+            builder.append(op);
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    public String textContent() {
+        StringBuilder builder = new StringBuilder();
+        for (Operation op : mList) {
+            String letter = "";
+            // if (op instanceof DrawTextRun) {
+            //   letter = "[" + ((DrawTextRun) op).text + "]";
+            // }
+            builder.append(letter);
+        }
+        return builder.toString();
+    }
+
+    public void debugBox(Component component, PaintContext context) {
+        float width = component.mWidth;
+        float height = component.mHeight;
+
+        context.savePaint();
+        mPaint.reset();
+        mPaint.setColor(0, 0, 255, 255); // Blue color
+        context.applyPaint(mPaint);
+        context.drawLine(0f, 0f, width, 0f);
+        context.drawLine(width, 0f, width, height);
+        context.drawLine(width, height, 0f, height);
+        context.drawLine(0f, height, 0f, 0f);
+        //        context.setColor(255, 0, 0, 255)
+        //        context.drawLine(0f, 0f, width, height)
+        //        context.drawLine(0f, height, width, 0f)
+        context.restorePaint();
+    }
+
+    public void setLayoutPosition(float x, float y) {
+        this.mX = x;
+        this.mY = y;
+    }
+
+    public float getTranslateX() {
+        if (mParent != null) {
+            return mX - mParent.mX;
+        }
+        return 0f;
+    }
+
+    public float getTranslateY() {
+        if (mParent != null) {
+            return mY - mParent.mY;
+        }
+        return 0f;
+    }
+
+    public void paintingComponent(PaintContext context) {
+        if (mPreTranslate != null) {
+            mPreTranslate.paint(context);
+        }
+        context.save();
+        context.translate(mX, mY);
+        if (context.isDebug()) {
+            debugBox(this, context);
+        }
+        for (Operation op : mList) {
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+        context.restore();
+    }
+
+    public boolean applyAnimationAsNeeded(PaintContext context) {
+        if (context.isAnimationEnabled() && mAnimateMeasure != null) {
+            mAnimateMeasure.apply(context);
+            needsRepaint();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        if (context.isDebug()) {
+            context.save();
+            context.translate(mX, mY);
+            context.savePaint();
+            mPaint.reset();
+            mPaint.setColor(0, 255, 0, 255); // Green
+            context.applyPaint(mPaint);
+            context.drawLine(0f, 0f, mWidth, 0f);
+            context.drawLine(mWidth, 0f, mWidth, mHeight);
+            context.drawLine(mWidth, mHeight, 0f, mHeight);
+            context.drawLine(0f, mHeight, 0f, 0f);
+            mPaint.setColor(255, 0, 0, 255); // Red
+            context.applyPaint(mPaint);
+            context.drawLine(0f, 0f, mWidth, mHeight);
+            context.drawLine(0f, mHeight, mWidth, 0f);
+            context.restorePaint();
+            context.restore();
+        }
+        if (applyAnimationAsNeeded(context)) {
+            return;
+        }
+        if (mVisibility == Visibility.GONE) {
+            return;
+        }
+        paintingComponent(context);
+    }
+
+    public void getComponents(ArrayList<Component> components) {
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                components.add((Component) op);
+            }
+        }
+    }
+
+    public int getComponentCount() {
+        int count = 0;
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                count += 1 + ((Component) op).getComponentCount();
+            }
+        }
+        return count;
+    }
+
+    public int getPaintId() {
+        if (mAnimationId != -1) {
+            return mAnimationId;
+        }
+        return mComponentId;
+    }
+
+    public boolean doesNeedsRepaint() {
+        return mNeedsRepaint;
+    }
+
+    public Component getComponent(int cid) {
+        if (mComponentId == cid || mAnimationId == cid) {
+            return this;
+        }
+        for (Operation c : mList) {
+            if (c instanceof Component) {
+                Component search = ((Component) c).getComponent(cid);
+                if (search != null) {
+                    return search;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
new file mode 100644
index 0000000..8a523a2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
@@ -0,0 +1,82 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+public class ComponentEnd implements Operation {
+
+    public static final ComponentEnd.Companion COMPANION = new ComponentEnd.Companion();
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT_END";
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "ComponentEnd";
+        }
+
+        @Override
+        public int id() {
+            return Operations.COMPONENT_END;
+        }
+
+        public static void apply(WireBuffer buffer) {
+            buffer.start(Operations.COMPONENT_END);
+        }
+
+        public static int size() {
+            return 1 + 4 + 4 + 4;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new ComponentEnd());
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("End tag for components / layouts. This operation marks the end"
+                            + "of a component");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
new file mode 100644
index 0000000..5cfad25
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
@@ -0,0 +1,193 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+public class ComponentStart implements ComponentStartOperation {
+
+    public static final ComponentStart.Companion COMPANION = new ComponentStart.Companion();
+
+    int mType = DEFAULT;
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    int mComponentId;
+
+    public int getType() {
+        return mType;
+    }
+
+    public float getX() {
+        return mX;
+    }
+
+    public float getY() {
+        return mY;
+    }
+
+    public float getWidth() {
+        return mWidth;
+    }
+
+    public float getHeight() {
+        return mHeight;
+    }
+
+    public int getComponentId() {
+        return mComponentId;
+    }
+
+    public ComponentStart(int type, int componentId, float width, float height) {
+        this.mType = type;
+        this.mComponentId = componentId;
+        this.mX = 0f;
+        this.mY = 0f;
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer, mType, mComponentId, mWidth, mHeight);
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT_START (type " + mType + " " + Companion.typeDescription(mType)
+                + ") - (" + mX + ", " + mY + " - " + mWidth + " x " + mHeight + ")";
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing
+    }
+
+    public static final int UNKNOWN = -1;
+    public static final int DEFAULT = 0;
+    public static final int ROOT_LAYOUT = 1;
+    public static final int LAYOUT = 2;
+    public static final int LAYOUT_CONTENT = 3;
+    public static final int SCROLL_CONTENT = 4;
+    public static final int BUTTON = 5;
+    public static final int CHECKBOX = 6;
+    public static final int TEXT = 7;
+    public static final int CURVED_TEXT = 8;
+    public static final int STATE_HOST = 9;
+    public static final int CUSTOM = 10;
+    public static final int LOTTIE = 11;
+    public static final int IMAGE = 12;
+    public static final int STATE_BOX_CONTENT = 13;
+    public static final int LAYOUT_BOX = 14;
+    public static final int LAYOUT_ROW = 15;
+    public static final int LAYOUT_COLUMN = 16;
+
+    public static class Companion implements DocumentedCompanionOperation {
+
+
+        public static String typeDescription(int type) {
+            switch (type) {
+                case DEFAULT:
+                    return "DEFAULT";
+                case ROOT_LAYOUT:
+                    return "ROOT_LAYOUT";
+                case LAYOUT:
+                    return "LAYOUT";
+                case LAYOUT_CONTENT:
+                    return "CONTENT";
+                case SCROLL_CONTENT:
+                    return "SCROLL_CONTENT";
+                case BUTTON:
+                    return "BUTTON";
+                case CHECKBOX:
+                    return "CHECKBOX";
+                case TEXT:
+                    return "TEXT";
+                case CURVED_TEXT:
+                    return "CURVED_TEXT";
+                case STATE_HOST:
+                    return "STATE_HOST";
+                case LOTTIE:
+                    return "LOTTIE";
+                case CUSTOM:
+                    return "CUSTOM";
+                case IMAGE:
+                    return "IMAGE";
+                default:
+                    return "UNKNOWN";
+            }
+        }
+
+        @Override
+        public String name() {
+            return "ComponentStart";
+        }
+
+        @Override
+        public int id() {
+            return Operations.COMPONENT_START;
+        }
+
+        public static void apply(WireBuffer buffer, int type, int componentId,
+                                 float width, float height) {
+            buffer.start(Operations.COMPONENT_START);
+            buffer.writeInt(type);
+            buffer.writeInt(componentId);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+        }
+
+        public static int size() {
+            return 1 + 4 + 4 + 4;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int type = buffer.readInt();
+            int componentId = buffer.readInt();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            operations.add(new ComponentStart(type, componentId, width, height));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Basic component encapsulating draw commands."
+                           + "This is not resizable.")
+                    .field(INT, "TYPE", "Type of components")
+                    .field(INT, "COMPONENT_ID", "unique id for this component")
+                    .field(FLOAT, "WIDTH", "width of the component")
+                    .field(FLOAT, "HEIGHT", "height of the component");
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java
similarity index 68%
rename from packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
rename to core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java
index 9a5c77a..67964ef 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.internal.widget.remotecompose.core.operations.layout;
 
-package com.android.systemui.shared.education
+import com.android.internal.widget.remotecompose.core.Operation;
 
-enum class GestureType {
-    BACK_GESTURE,
+public interface ComponentStartOperation extends Operation {
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
new file mode 100644
index 0000000..941666a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
@@ -0,0 +1,27 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * Indicates a lightweight component (without children) that is only laid out and not able to be
+ * measured. Eg borders, background, clips, etc.
+ */
+public interface DecoratorComponent {
+    void layout(RemoteContext context, float width, float height);
+    void onClick(float x, float y);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
new file mode 100644
index 0000000..f198c4a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
@@ -0,0 +1,220 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DimensionModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthModifierOperation;
+
+import java.util.ArrayList;
+
+/**
+ * Component with modifiers and children
+ */
+public class LayoutComponent extends Component {
+
+    protected WidthModifierOperation mWidthModifier = null;
+    protected HeightModifierOperation mHeightModifier = null;
+
+    // Margins
+    protected float mMarginLeft = 0f;
+    protected float mMarginRight = 0f;
+    protected float mMarginTop = 0f;
+    protected float mMarginBottom = 0f;
+
+    protected float mPaddingLeft = 0f;
+    protected float mPaddingRight = 0f;
+    protected float mPaddingTop = 0f;
+    protected float mPaddingBottom = 0f;
+
+    protected ComponentModifiers mComponentModifiers = new ComponentModifiers();
+    protected ArrayList<Component> mChildrenComponents = new ArrayList<>();
+
+    public LayoutComponent(Component parent, int componentId, int animationId,
+                           float x, float y, float width, float height) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public float getMarginLeft() {
+        return mMarginLeft;
+    }
+    public float getMarginRight() {
+        return mMarginRight;
+    }
+    public float getMarginTop() {
+        return mMarginTop;
+    }
+    public float getMarginBottom() {
+        return mMarginBottom;
+    }
+
+    public WidthModifierOperation getWidthModifier() {
+        return mWidthModifier;
+    }
+    public HeightModifierOperation getHeightModifier() {
+        return mHeightModifier;
+    }
+
+    public void inflate() {
+        for (Operation op : mList) {
+            if (op instanceof LayoutComponentContent) {
+                ((LayoutComponentContent) op).mParent = this;
+                mChildrenComponents.clear();
+                ((LayoutComponentContent) op).getComponents(mChildrenComponents);
+                if (mChildrenComponents.isEmpty()) {
+                    mChildrenComponents.add((Component) op);
+                }
+            } else if (op instanceof ModifierOperation) {
+                mComponentModifiers.add((ModifierOperation) op);
+            } else {
+                // nothing
+            }
+        }
+
+        mList.clear();
+        mList.add(mComponentModifiers);
+        for (Component c : mChildrenComponents) {
+            c.mParent = this;
+            mList.add(c);
+        }
+
+        mX = 0f;
+        mY = 0f;
+        mMarginLeft = 0f;
+        mMarginTop = 0f;
+        mMarginRight = 0f;
+        mMarginBottom = 0f;
+        mPaddingLeft = 0f;
+        mPaddingTop = 0f;
+        mPaddingRight = 0f;
+        mPaddingBottom = 0f;
+
+        boolean applyHorizontalMargin = true;
+        boolean applyVerticalMargin = true;
+        for (Operation op : mComponentModifiers.getList()) {
+            if (op instanceof PaddingModifierOperation) {
+                // We are accumulating padding modifiers to compute the margin
+                // until we hit a dimension; the computed padding for the
+                // content simply accumulate all the padding modifiers.
+                float left = ((PaddingModifierOperation) op).getLeft();
+                float right = ((PaddingModifierOperation) op).getRight();
+                float top = ((PaddingModifierOperation) op).getTop();
+                float bottom = ((PaddingModifierOperation) op).getBottom();
+                if (applyHorizontalMargin) {
+                    mMarginLeft += left;
+                    mMarginRight += right;
+                }
+                if (applyVerticalMargin) {
+                    mMarginTop += top;
+                    mMarginBottom += bottom;
+                }
+                mPaddingLeft += left;
+                mPaddingTop += top;
+                mPaddingRight += right;
+                mPaddingBottom += bottom;
+            }
+            if (op instanceof WidthModifierOperation && mWidthModifier == null) {
+                mWidthModifier = (WidthModifierOperation) op;
+                applyHorizontalMargin = false;
+            }
+            if (op instanceof HeightModifierOperation && mHeightModifier == null) {
+                mHeightModifier = (HeightModifierOperation) op;
+                applyVerticalMargin = false;
+            }
+        }
+        if (mWidthModifier == null) {
+            mWidthModifier = new WidthModifierOperation(DimensionModifierOperation.Type.WRAP);
+        }
+        if (mHeightModifier == null) {
+            mHeightModifier = new HeightModifierOperation(DimensionModifierOperation.Type.WRAP);
+        }
+        mWidth = computeModifierDefinedWidth();
+        mHeight = computeModifierDefinedHeight();
+    }
+
+    @Override
+    public String toString() {
+        return "UNKNOWN LAYOUT_COMPONENT";
+    }
+
+    @Override
+    public void paintingComponent(PaintContext context) {
+        context.save();
+        context.translate(mX, mY);
+        mComponentModifiers.paint(context);
+        float tx = mPaddingLeft;
+        float ty = mPaddingTop;
+        context.translate(tx, ty);
+        for (Component child : mChildrenComponents) {
+            child.paint(context);
+        }
+        context.translate(-tx, -ty);
+        context.restore();
+    }
+
+    /**
+     * Traverse the modifiers to compute indicated dimension
+     */
+    public float computeModifierDefinedWidth() {
+        float s = 0f;
+        float e = 0f;
+        float w = 0f;
+        for (Operation c : mComponentModifiers.getList()) {
+            if (c instanceof WidthModifierOperation) {
+                WidthModifierOperation o = (WidthModifierOperation) c;
+                if (o.getType() == DimensionModifierOperation.Type.EXACT) {
+                    w = o.getValue();
+                }
+                break;
+            }
+            if (c instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) c;
+                s += pop.getLeft();
+                e += pop.getRight();
+            }
+        }
+        return s + w + e;
+    }
+
+    /**
+     * Traverse the modifiers to compute indicated dimension
+     */
+    public float computeModifierDefinedHeight() {
+        float t = 0f;
+        float b = 0f;
+        float h = 0f;
+        for (Operation c : mComponentModifiers.getList()) {
+            if (c instanceof HeightModifierOperation) {
+                HeightModifierOperation o = (HeightModifierOperation) c;
+                if (o.getType() == DimensionModifierOperation.Type.EXACT) {
+                    h = o.getValue();
+                }
+                break;
+            }
+            if (c instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) c;
+                t += pop.getTop();
+                b += pop.getBottom();
+            }
+        }
+        return t + h + b;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
new file mode 100644
index 0000000..769ff6a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
@@ -0,0 +1,68 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+/**
+ * Represents the content of a LayoutComponent (i.e. the children components)
+ */
+public class LayoutComponentContent extends Component implements ComponentStartOperation {
+
+    public static final LayoutComponentContent.Companion COMPANION =
+            new LayoutComponentContent.Companion();
+
+    public LayoutComponentContent(int componentId, float x, float y,
+                                  float width, float height, Component parent, int animationId) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "LayoutContent";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_CONTENT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.LAYOUT_CONTENT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new LayoutComponentContent(
+                    -1, 0, 0, 0, 0, null, -1));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Container for components. BoxLayout, RowLayout and ColumnLayout "
+                           + "expects a LayoutComponentContent as a child, encapsulating the "
+                           + "components that needs to be laid out.");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
new file mode 100644
index 0000000..dc13768
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
@@ -0,0 +1,173 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Represents the root layout component. Entry point to the component tree layout/paint.
+ */
+public class RootLayoutComponent extends Component implements ComponentStartOperation {
+
+    public static final RootLayoutComponent.Companion COMPANION =
+            new RootLayoutComponent.Companion();
+
+    int mCurrentId = -1;
+
+    public RootLayoutComponent(int componentId, float x, float y,
+                               float width, float height, Component parent, int animationId) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public RootLayoutComponent(int componentId, float x, float y,
+                               float width, float height, Component parent) {
+        super(parent, componentId, -1, x, y, width, height);
+    }
+
+    @Override
+    public String toString() {
+        return "ROOT (" + mX + ", " + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "ROOT [" + mComponentId + ":" + mAnimationId
+                + "] = [" + mX + ", " + mY + ", " + mWidth + ", " + mHeight + "] " + mVisibility);
+    }
+
+    public int getNextId() {
+        mCurrentId--;
+        return mCurrentId;
+    }
+
+    public void assignIds() {
+        assignId(this);
+    }
+
+    void assignId(Component component) {
+        if (component.mComponentId == -1) {
+            component.mComponentId = getNextId();
+        }
+        for (Operation op : component.mList) {
+            if (op instanceof Component) {
+                assignId((Component) op);
+            }
+        }
+    }
+
+    /**
+     * This will measure then layout the tree of components
+     */
+    public void layout(RemoteContext context) {
+        if (!mNeedsMeasure) {
+            return;
+        }
+        context.lastComponent = this;
+        mWidth = context.mWidth;
+        mHeight = context.mHeight;
+
+        // TODO: reuse MeasurePass
+        MeasurePass measurePass = new MeasurePass();
+        for (Operation op : mList) {
+            if (op instanceof Measurable) {
+                Measurable m = (Measurable) op;
+                m.measure(context.getPaintContext(),
+                        0f, mWidth, 0f, mHeight, measurePass);
+                m.layout(context, measurePass);
+            }
+        }
+        mNeedsMeasure = false;
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        mNeedsRepaint = false;
+        context.getContext().lastComponent = this;
+        context.save();
+
+        if (mParent == null) { // root layout
+            context.clipRect(0f, 0f, mWidth, mHeight);
+        }
+
+        for (Operation op : mList) {
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+
+        context.restore();
+    }
+
+    public String displayHierarchy() {
+        StringSerializer serializer = new StringSerializer();
+        displayHierarchy(this, 0, serializer);
+        return serializer.toString();
+    }
+
+    public void displayHierarchy(Component component, int indent, StringSerializer serializer) {
+        component.serializeToString(indent, serializer);
+        for (Operation c : component.mList) {
+            if (c instanceof ComponentModifiers) {
+                ((ComponentModifiers) c).serializeToString(indent + 1, serializer);
+            }
+            if (c instanceof Component) {
+                displayHierarchy((Component) c, indent + 1, serializer);
+            }
+        }
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "RootLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_ROOT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.LAYOUT_ROOT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new RootLayoutComponent(
+                    -1, 0, 0, 0, 0, null, -1));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Root element for a document. Other components / layout managers "
+                         + "are children in the component tree starting from this Root component.");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
new file mode 100644
index 0000000..7c6bef4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
@@ -0,0 +1,311 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.GeneralEasing;
+
+/**
+ * Basic interpolation manager between two ComponentMeasures
+ *
+ * Handles position, size and visibility
+ */
+public class AnimateMeasure {
+    long mStartTime = System.currentTimeMillis();
+    Component mComponent;
+    ComponentMeasure mOriginal;
+    ComponentMeasure mTarget;
+    int mDuration;
+    int mDurationVisibilityChange = mDuration;
+    AnimationSpec.ANIMATION mEnterAnimation = AnimationSpec.ANIMATION.FADE_IN;
+    AnimationSpec.ANIMATION mExitAnimation = AnimationSpec.ANIMATION.FADE_OUT;
+    int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
+    int mVisibilityEasingType = GeneralEasing.CUBIC_ACCELERATE;
+
+    float mP = 0f;
+    float mVp = 0f;
+    FloatAnimation mMotionEasing = new FloatAnimation(mMotionEasingType,
+            mDuration / 1000f, null, 0f, Float.NaN);
+    FloatAnimation mVisibilityEasing = new FloatAnimation(mVisibilityEasingType,
+            mDurationVisibilityChange / 1000f,
+            null, 0f, Float.NaN);
+    ParticleAnimation mParticleAnimation;
+
+    public AnimateMeasure(long startTime, Component component, ComponentMeasure original,
+                          ComponentMeasure target, int duration, int durationVisibilityChange,
+                          AnimationSpec.ANIMATION enterAnimation,
+                          AnimationSpec.ANIMATION exitAnimation,
+                          int motionEasingType, int visibilityEasingType) {
+        this.mStartTime = startTime;
+        this.mComponent = component;
+        this.mOriginal = original;
+        this.mTarget = target;
+        this.mDuration = duration;
+        this.mDurationVisibilityChange = durationVisibilityChange;
+        this.mEnterAnimation = enterAnimation;
+        this.mExitAnimation = exitAnimation;
+
+        mMotionEasing.setTargetValue(1f);
+        mVisibilityEasing.setTargetValue(1f);
+        component.mVisibility = target.getVisibility();
+    }
+
+    public void update(long currentTime) {
+        long elapsed = currentTime - mStartTime;
+        mP = Math.min(elapsed / (float) mDuration, 1f);
+        //mP = motionEasing.get(mP);
+        mVp = Math.min(elapsed / (float) mDurationVisibilityChange, 1f);
+        mVp = mVisibilityEasing.get(mVp);
+    }
+
+    public PaintBundle paint = new PaintBundle();
+
+    public void apply(PaintContext context) {
+        update(context.getContext().currentTime);
+
+        mComponent.setX(getX());
+        mComponent.setY(getY());
+        mComponent.setWidth(getWidth());
+        mComponent.setHeight(getHeight());
+
+        float w = mComponent.getWidth();
+        float h = mComponent.getHeight();
+        for (Operation op : mComponent.mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                w -= pop.getLeft() + pop.getRight();
+                h -= pop.getTop() + pop.getBottom();
+            }
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).layout(context.getContext(), w, h);
+            }
+        }
+
+        mComponent.mVisibility = mTarget.getVisibility();
+        if (mOriginal.getVisibility() != mTarget.getVisibility()) {
+            if (mTarget.getVisibility() == Component.Visibility.GONE) {
+                switch (mExitAnimation) {
+                    case PARTICLE:
+                        // particleAnimation(context, component, original, target, vp)
+                        if (mParticleAnimation == null) {
+                            mParticleAnimation = new ParticleAnimation();
+                        }
+                        mParticleAnimation.animate(context, mComponent, mOriginal, mTarget, mVp);
+                        break;
+                    case FADE_OUT:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, 1f - mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_LEFT:
+                        context.save();
+                        context.translate(-mVp * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_RIGHT:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, 1f);
+                        context.applyPaint(paint);
+                        context.translate(mVp * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_TOP:
+                        context.save();
+                        context.translate(0f,
+                                -mVp * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_BOTTOM:
+                        context.save();
+                        context.translate(0f,
+                                mVp * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    default:
+                        //            particleAnimation(context, component, original, target, vp)
+                        if (mParticleAnimation == null) {
+                            mParticleAnimation = new ParticleAnimation();
+                        }
+                        mParticleAnimation.animate(context, mComponent, mOriginal, mTarget, mVp);
+                        break;
+                }
+            } else if (mOriginal.getVisibility() == Component.Visibility.GONE
+                    && mTarget.getVisibility() == Component.Visibility.VISIBLE) {
+                switch (mEnterAnimation) {
+                    case ROTATE:
+                        float px = mTarget.getX() + mTarget.getW() / 2f;
+                        float py = mTarget.getY() + mTarget.getH() / 2f;
+
+                        context.save();
+                        context.savePaint();
+                        context.matrixRotate(mVp * 360f, px, py);
+                        context.matrixScale(1f * mVp, 1f * mVp, px, py);
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case FADE_IN:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_LEFT:
+                        context.save();
+                        context.translate(
+                                (1f - mVp) * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_RIGHT:
+                        context.save();
+                        context.translate(
+                                -(1f - mVp) * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_TOP:
+                        context.save();
+                        context.translate(0f,
+                                (1f - mVp) * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_BOTTOM:
+                        context.save();
+                        context.translate(0f,
+                                -(1f - mVp) * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                mComponent.paintingComponent(context);
+            }
+        } else {
+            mComponent.paintingComponent(context);
+        }
+
+        if (mP >= 1f && mVp >= 1f) {
+            mComponent.mAnimateMeasure = null;
+            mComponent.mVisibility = mTarget.getVisibility();
+        }
+    }
+
+    public boolean isDone() {
+        return mP >= 1f && mVp >= 1f;
+    }
+
+    public float getX() {
+        return mOriginal.getX() * (1 - mP) + mTarget.getX() * mP;
+    }
+
+    public float getY() {
+        return mOriginal.getY() * (1 - mP) + mTarget.getY() * mP;
+    }
+
+    public float getWidth() {
+        return mOriginal.getW() * (1 - mP) + mTarget.getW() * mP;
+    }
+
+    public float getHeight() {
+        return mOriginal.getH() * (1 - mP) + mTarget.getH() * mP;
+    }
+
+    public float getVisibility() {
+        if (mOriginal.getVisibility() == mTarget.getVisibility()) {
+            return 1f;
+        } else if (mTarget.getVisibility() == Component.Visibility.VISIBLE) {
+            return mVp;
+        } else {
+            return 1 - mVp;
+        }
+    }
+
+    public void updateTarget(ComponentMeasure measure, long currentTime) {
+        mOriginal.setX(getX());
+        mOriginal.setY(getY());
+        mOriginal.setW(getWidth());
+        mOriginal.setH(getHeight());
+        mTarget.setX(measure.getX());
+        mTarget.setY(measure.getY());
+        mTarget.setW(measure.getW());
+        mTarget.setH(measure.getH());
+        mTarget.setVisibility(measure.getVisibility());
+        mStartTime = currentTime;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
new file mode 100644
index 0000000..386d365
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
@@ -0,0 +1,186 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.GeneralEasing;
+
+import java.util.List;
+
+/**
+ * Basic component animation spec
+ */
+public class AnimationSpec implements Operation {
+
+    public static final AnimationSpec.Companion COMPANION = new AnimationSpec.Companion();
+
+    int mAnimationId = -1;
+    int mMotionDuration = 300;
+    int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
+    int mVisibilityDuration = 300;
+    int mVisibilityEasingType = GeneralEasing.CUBIC_STANDARD;
+    ANIMATION mEnterAnimation = ANIMATION.FADE_IN;
+    ANIMATION mExitAnimation = ANIMATION.FADE_OUT;
+
+    public AnimationSpec(int animationId, int motionDuration, int motionEasingType,
+                         int visibilityDuration, int visibilityEasingType,
+                         ANIMATION enterAnimation, ANIMATION exitAnimation) {
+        this.mAnimationId = animationId;
+        this.mMotionDuration = motionDuration;
+        this.mMotionEasingType = motionEasingType;
+        this.mVisibilityDuration = visibilityDuration;
+        this.mVisibilityEasingType = visibilityEasingType;
+        this.mEnterAnimation = enterAnimation;
+        this.mExitAnimation = exitAnimation;
+    }
+
+    public AnimationSpec() {
+        this(-1, 300, GeneralEasing.CUBIC_STANDARD,
+                300, GeneralEasing.CUBIC_STANDARD,
+                ANIMATION.FADE_IN, ANIMATION.FADE_OUT);
+    }
+
+    public int getAnimationId() {
+        return mAnimationId;
+    }
+
+    public int getMotionDuration() {
+        return mMotionDuration;
+    }
+
+    public int getMotionEasingType() {
+        return mMotionEasingType;
+    }
+
+    public int getVisibilityDuration() {
+        return mVisibilityDuration;
+    }
+
+    public int getVisibilityEasingType() {
+        return mVisibilityEasingType;
+    }
+
+    public ANIMATION getEnterAnimation() {
+        return mEnterAnimation;
+    }
+
+    public ANIMATION getExitAnimation() {
+        return mExitAnimation;
+    }
+
+    @Override
+    public String toString() {
+        return "ANIMATION_SPEC (" + mMotionDuration + " ms)";
+    }
+
+    public enum ANIMATION {
+        FADE_IN,
+        FADE_OUT,
+        SLIDE_LEFT,
+        SLIDE_RIGHT,
+        SLIDE_TOP,
+        SLIDE_BOTTOM,
+        ROTATE,
+        PARTICLE
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer, mAnimationId, mMotionDuration, mMotionEasingType,
+                mVisibilityDuration, mVisibilityEasingType, mEnterAnimation, mExitAnimation);
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing here
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    public static class Companion implements CompanionOperation {
+        @Override
+        public String name() {
+            return "AnimationSpec";
+        }
+
+        @Override
+        public int id() {
+            return Operations.ANIMATION_SPEC;
+        }
+
+        public static int animationToInt(ANIMATION animation) {
+            return animation.ordinal();
+        }
+
+        public static ANIMATION intToAnimation(int value) {
+            switch (value) {
+                case 0:
+                    return ANIMATION.FADE_IN;
+                case 1:
+                    return ANIMATION.FADE_OUT;
+                case 2:
+                    return ANIMATION.SLIDE_LEFT;
+                case 3:
+                    return ANIMATION.SLIDE_RIGHT;
+                case 4:
+                    return ANIMATION.SLIDE_TOP;
+                case 5:
+                    return ANIMATION.SLIDE_BOTTOM;
+                case 6:
+                    return ANIMATION.ROTATE;
+                case 7:
+                    return ANIMATION.PARTICLE;
+                default:
+                    return ANIMATION.FADE_IN;
+            }
+        }
+
+        public static void apply(WireBuffer buffer, int animationId, int motionDuration,
+                                 int motionEasingType, int visibilityDuration,
+                                 int visibilityEasingType, ANIMATION enterAnimation,
+                                 ANIMATION exitAnimation) {
+            buffer.start(Operations.ANIMATION_SPEC);
+            buffer.writeInt(animationId);
+            buffer.writeInt(motionDuration);
+            buffer.writeInt(motionEasingType);
+            buffer.writeInt(visibilityDuration);
+            buffer.writeInt(visibilityEasingType);
+            buffer.writeInt(animationToInt(enterAnimation));
+            buffer.writeInt(animationToInt(exitAnimation));
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int animationId = buffer.readInt();
+            int motionDuration = buffer.readInt();
+            int motionEasingType = buffer.readInt();
+            int visibilityDuration = buffer.readInt();
+            int visibilityEasingType = buffer.readInt();
+            ANIMATION enterAnimation = intToAnimation(buffer.readInt());
+            ANIMATION exitAnimation = intToAnimation(buffer.readInt());
+            AnimationSpec op = new AnimationSpec(animationId, motionDuration, motionEasingType,
+                    visibilityDuration, visibilityEasingType, enterAnimation, exitAnimation);
+            operations.add(op);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java
new file mode 100644
index 0000000..4562692
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java
@@ -0,0 +1,34 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+public class Particle {
+    public final float x;
+    public final float y;
+    public float radius;
+    public float r;
+    public float g;
+    public float b;
+
+    public Particle(float x, float y, float radius, float r, float g, float b) {
+        this.x = x;
+        this.y = y;
+        this.radius = radius;
+        this.r = r;
+        this.g = g;
+        this.b = b;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
new file mode 100644
index 0000000..5c5d056
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class ParticleAnimation {
+    HashMap<Integer, ArrayList<Particle>> mAllParticles = new HashMap<>();
+
+    PaintBundle mPaint = new PaintBundle();
+    public void animate(PaintContext context, Component component,
+                        ComponentMeasure start, ComponentMeasure end,
+                        float progress) {
+        ArrayList<Particle> particles = mAllParticles.get(component.getComponentId());
+        if (particles == null) {
+            particles = new ArrayList<Particle>();
+            for (int i = 0; i < 20; i++) {
+                float x = (float) Math.random();
+                float y = (float) Math.random();
+                float radius = (float) Math.random();
+                float r = 250f;
+                float g = 250f;
+                float b = 250f;
+                particles.add(new Particle(x, y, radius, r, g, b));
+            }
+            mAllParticles.put(component.getComponentId(), particles);
+        }
+        context.save();
+        context.savePaint();
+        for (int i = 0; i < particles.size(); i++) {
+            Particle particle = particles.get(i);
+            mPaint.reset();
+            mPaint.setColor(particle.r, particle.g, particle.b,
+                    200 * (1 - progress));
+            context.applyPaint(mPaint);
+            float dx = start.getX() + component.getWidth() * particle.x;
+            float dy = start.getY() + component.getHeight() * particle.y
+                    + progress * 0.01f * component.getHeight();
+            float dr = (component.getHeight() + 60) * 0.15f * particle.radius + (30 * progress);
+            context.drawCircle(dx, dy, dr);
+        }
+        context.restorePaint();
+        context.restore();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
new file mode 100644
index 0000000..fea8dd2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+
+import java.util.List;
+
+/**
+ * Simple Box layout implementation
+ */
+public class BoxLayout extends LayoutManager implements ComponentStartOperation {
+
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+
+    public static final BoxLayout.Companion COMPANION = new BoxLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+
+    public BoxLayout(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height,
+                     int horizontalPositioning, int verticalPositioning) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+    }
+
+    public BoxLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning);
+    }
+
+    @Override
+    public String toString() {
+        return "BOX [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "BOX";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth, 0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(Math.max(size.getWidth(), m.getW()));
+            size.setHeight(Math.max(size.getHeight(), m.getH()));
+        }
+        // add padding
+        size.setWidth(Math.max(size.getWidth(), computeModifierDefinedWidth()));
+        size.setHeight(Math.max(size.getHeight(), computeModifierDefinedHeight()));
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, maxWidth, minHeight, maxHeight, measure);
+        }
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure m = measure.get(child);
+            float tx = 0f;
+            float ty = 0f;
+            switch (mVerticalPositioning) {
+                case TOP:
+                    ty = 0f;
+                    break;
+                case CENTER:
+                    ty = (selfHeight - m.getH()) / 2f;
+                    break;
+                case BOTTOM:
+                    ty = selfHeight - m.getH();
+                    break;
+            }
+            switch (mHorizontalPositioning) {
+                case START:
+                    tx = 0f;
+                    break;
+                case CENTER:
+                    tx = (selfWidth - m.getW()) / 2f;
+                    break;
+                case END:
+                    tx = selfWidth - m.getW();
+                    break;
+            }
+            m.setX(tx);
+            m.setY(ty);
+            m.setVisibility(child.mVisibility);
+        }
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "BoxLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_BOX;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning) {
+            buffer.start(Operations.LAYOUT_BOX);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            operations.add(new BoxLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                .description("Box layout implementation.\n\n"
+                      + "Child components are laid out independently from one another,\n"
+                      + " and painted in their hierarchy order (first children drawn"
+                      + "before the latter). Horizontal and Vertical positioning"
+                      + "are supported.")
+                .examplesDimension(150, 100)
+                .exampleImage("Top", "layout-BoxLayout-start-top.png")
+                .exampleImage("Center", "layout-BoxLayout-center-center.png")
+                .exampleImage("Bottom", "layout-BoxLayout-end-bottom.png")
+                .field(INT, "COMPONENT_ID", "unique id for this component")
+                .field(INT, "ANIMATION_ID", "id used to match components,"
+                      + " for animation purposes")
+                .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+                    .possibleValues("START", BoxLayout.START)
+                    .possibleValues("CENTER", BoxLayout.CENTER)
+                    .possibleValues("END", BoxLayout.END)
+                .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+                    .possibleValues("TOP", BoxLayout.TOP)
+                    .possibleValues("CENTER", BoxLayout.CENTER)
+                    .possibleValues("BOTTOM", BoxLayout.BOTTOM);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
new file mode 100644
index 0000000..a1a2de5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
@@ -0,0 +1,291 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog;
+
+import java.util.List;
+
+/**
+ * Simple Column layout implementation
+ * - also supports weight and horizontal/vertical positioning
+ */
+public class ColumnLayout extends LayoutManager implements ComponentStartOperation {
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+    public static final int SPACE_BETWEEN = 6;
+    public static final int SPACE_EVENLY = 7;
+    public static final int SPACE_AROUND = 8;
+
+    public static final ColumnLayout.Companion COMPANION = new ColumnLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+    float mSpacedBy = 0f;
+
+    public ColumnLayout(Component parent, int componentId, int animationId,
+                        float x, float y, float width, float height,
+                        int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+        mSpacedBy = spacedBy;
+    }
+
+    public ColumnLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning, spacedBy);
+    }
+
+    @Override
+    public String toString() {
+        return "COLUMN [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "COLUMN";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        DebugLog.s(() -> "COMPUTE WRAP SIZE in " + this + " (" + mComponentId + ")");
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth,
+                    0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(Math.max(size.getWidth(), m.getW()));
+            size.setHeight(size.getHeight() + m.getH());
+        }
+        if (!mChildrenComponents.isEmpty()) {
+            size.setHeight(size.getHeight()
+                    + (mSpacedBy * (mChildrenComponents.size() - 1)));
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        DebugLog.s(() -> "COMPUTE SIZE in " + this + " (" + mComponentId + ")");
+        float mh = maxHeight;
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, maxWidth, minHeight, mh, measure);
+            ComponentMeasure m = measure.get(child);
+            mh -= m.getH();
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        DebugLog.s(() -> "INTERNAL LAYOUT " + this + " (" + mComponentId + ") children: "
+                + mChildrenComponents.size() + " size (" + selfMeasure.getW()
+                + " x " + selfMeasure.getH() + ")");
+        if (mChildrenComponents.isEmpty()) {
+            DebugLog.e();
+            return;
+        }
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        float childrenWidth = 0f;
+        float childrenHeight = 0f;
+
+        boolean hasWeights = false;
+        float totalWeights = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            if (child instanceof LayoutComponent
+                    && ((LayoutComponent) child).getHeightModifier().hasWeight()) {
+                hasWeights = true;
+                totalWeights += ((LayoutComponent) child).getHeightModifier().getValue();
+            } else {
+                childrenHeight += childMeasure.getH();
+            }
+        }
+        if (hasWeights) {
+            float availableSpace = selfHeight - childrenHeight;
+            for (Component child : mChildrenComponents) {
+                if (child instanceof LayoutComponent
+                        && ((LayoutComponent) child).getHeightModifier().hasWeight()) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    float weight = ((LayoutComponent) child).getHeightModifier().getValue();
+                    childMeasure.setH((weight * availableSpace) / totalWeights);
+                    child.measure(context, childMeasure.getW(),
+                            childMeasure.getW(), childMeasure.getH(), childMeasure.getH(), measure);
+                }
+            }
+        }
+
+        childrenHeight = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            childrenWidth = Math.max(childrenWidth, childMeasure.getW());
+            childrenHeight += childMeasure.getH();
+        }
+        childrenHeight += mSpacedBy * (mChildrenComponents.size() - 1);
+
+        float tx = 0f;
+        float ty = 0f;
+
+        float verticalGap = 0f;
+        float total = 0f;
+        switch (mVerticalPositioning) {
+            case TOP:
+                ty = 0f;
+                break;
+            case CENTER:
+                ty = (selfHeight - childrenHeight) / 2f;
+                break;
+            case BOTTOM:
+                ty = selfHeight - childrenHeight;
+                break;
+            case SPACE_BETWEEN:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size() - 1);
+                break;
+            case SPACE_EVENLY:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size() + 1);
+                ty = verticalGap;
+                break;
+            case SPACE_AROUND:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size());
+                ty = verticalGap / 2f;
+                break;
+        }
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            switch (mHorizontalPositioning) {
+                case START:
+                    tx = 0f;
+                    break;
+                case CENTER:
+                    tx = (selfWidth - childMeasure.getW()) / 2f;
+                    break;
+                case END:
+                    tx = selfWidth - childMeasure.getW();
+                    break;
+            }
+            childMeasure.setX(tx);
+            childMeasure.setY(ty);
+            childMeasure.setVisibility(child.mVisibility);
+            ty += childMeasure.getH();
+            if (mVerticalPositioning == SPACE_BETWEEN
+                    || mVerticalPositioning == SPACE_AROUND
+                    || mVerticalPositioning == SPACE_EVENLY) {
+                ty += verticalGap;
+            }
+            ty += mSpacedBy;
+        }
+        DebugLog.e();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "ColumnLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_COLUMN;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning, float spacedBy) {
+            buffer.start(Operations.LAYOUT_COLUMN);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+            buffer.writeFloat(spacedBy);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            float spacedBy = buffer.readFloat();
+            operations.add(new ColumnLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning, spacedBy));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+               .description("Column layout implementation, positioning components one"
+                       + " after the other vertically.\n\n"
+                       + "It supports weight and horizontal/vertical positioning.")
+               .examplesDimension(100, 400)
+               .exampleImage("Top", "layout-ColumnLayout-start-top.png")
+               .exampleImage("Center", "layout-ColumnLayout-start-center.png")
+               .exampleImage("Bottom", "layout-ColumnLayout-start-bottom.png")
+               .exampleImage("SpaceEvenly", "layout-ColumnLayout-start-space-evenly.png")
+               .exampleImage("SpaceAround", "layout-ColumnLayout-start-space-around.png")
+               .exampleImage("SpaceBetween", "layout-ColumnLayout-start-space-between.png")
+               .field(INT, "COMPONENT_ID", "unique id for this component")
+               .field(INT, "ANIMATION_ID", "id used to match components,"
+                       + " for animation purposes")
+               .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+               .possibleValues("START", ColumnLayout.START)
+               .possibleValues("CENTER", ColumnLayout.CENTER)
+               .possibleValues("END", ColumnLayout.END)
+               .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+               .possibleValues("TOP", ColumnLayout.TOP)
+               .possibleValues("CENTER", ColumnLayout.CENTER)
+               .possibleValues("BOTTOM", ColumnLayout.BOTTOM)
+               .possibleValues("SPACE_BETWEEN", ColumnLayout.SPACE_BETWEEN)
+               .possibleValues("SPACE_EVENLY", ColumnLayout.SPACE_EVENLY)
+               .possibleValues("SPACE_AROUND", ColumnLayout.SPACE_AROUND)
+                    .field(FLOAT, "SPACED_BY", "Horizontal spacing between components");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
new file mode 100644
index 0000000..4890683
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
@@ -0,0 +1,136 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.managers;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+
+/**
+ * Base class for layout managers -- resizable components.
+ */
+public abstract class LayoutManager extends LayoutComponent implements Measurable {
+
+    Size mCachedWrapSize = new Size(0f, 0f);
+
+    public LayoutManager(Component parent, int componentId, int animationId,
+                         float x, float y, float width, float height) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    /**
+     * Implemented by subclasses to provide a layout/measure pass
+     */
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        // nothing here
+    }
+
+    /**
+     * Subclasses can implement this to provide wrap sizing
+     */
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        // nothing here
+    }
+
+    /**
+     * Subclasses can implement this when not in wrap sizing
+     */
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        // nothing here
+    }
+
+    /**
+     * Base implementation of the measure resolution
+     */
+    @Override
+    public void measure(PaintContext context, float minWidth, float maxWidth,
+                        float minHeight, float maxHeight, MeasurePass measure) {
+        boolean hasWrap = true;
+        float measuredWidth = Math.min(maxWidth,
+                computeModifierDefinedWidth() - mMarginLeft - mMarginRight);
+        float measuredHeight = Math.min(maxHeight,
+                computeModifierDefinedHeight() - mMarginTop - mMarginBottom);
+        float insetMaxWidth = maxWidth - mMarginLeft - mMarginRight;
+        float insetMaxHeight = maxHeight - mMarginTop - mMarginBottom;
+        if (mWidthModifier.isWrap() || mHeightModifier.isWrap()) {
+            mCachedWrapSize.setWidth(0f);
+            mCachedWrapSize.setHeight(0f);
+            computeWrapSize(context, maxWidth, maxHeight, measure, mCachedWrapSize);
+            measuredWidth = mCachedWrapSize.getWidth();
+            measuredHeight = mCachedWrapSize.getHeight();
+        } else {
+            hasWrap = false;
+        }
+        if (mWidthModifier.isFill()) {
+            measuredWidth = insetMaxWidth;
+        } else if (mWidthModifier.hasWeight()) {
+            measuredWidth = Math.max(measuredWidth, computeModifierDefinedWidth());
+        } else {
+            measuredWidth = Math.max(measuredWidth, minWidth);
+            measuredWidth = Math.min(measuredWidth, insetMaxWidth);
+        }
+        if (mHeightModifier.isFill()) {
+            measuredHeight = insetMaxHeight;
+        } else if (mHeightModifier.hasWeight()) {
+            measuredHeight = Math.max(measuredHeight, computeModifierDefinedHeight());
+        } else {
+            measuredHeight = Math.max(measuredHeight, minHeight);
+            measuredHeight = Math.min(measuredHeight, insetMaxHeight);
+        }
+        if (minWidth == maxWidth) {
+            measuredWidth = maxWidth;
+        }
+        if (minHeight == maxHeight) {
+            measuredHeight = maxHeight;
+        }
+        measuredWidth = Math.min(measuredWidth, insetMaxWidth);
+        measuredHeight = Math.min(measuredHeight, insetMaxHeight);
+        if (!hasWrap) {
+            computeSize(context, 0f, measuredWidth, 0f, measuredHeight, measure);
+        }
+        measuredWidth += mMarginLeft + mMarginRight;
+        measuredHeight += mMarginTop + mMarginBottom;
+
+        ComponentMeasure m = measure.get(this);
+        m.setW(measuredWidth);
+        m.setH(measuredHeight);
+
+        internalLayoutMeasure(context, measure);
+    }
+
+    /**
+     * basic layout of internal components
+     */
+    @Override
+    public void layout(RemoteContext context, MeasurePass measure) {
+        super.layout(context, measure);
+        ComponentMeasure self = measure.get(this);
+
+        mComponentModifiers.layout(context, self.getW(), self.getH());
+        for (Component c : mChildrenComponents) {
+            c.layout(context, measure);
+        }
+        this.mNeedsMeasure = false;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
new file mode 100644
index 0000000..07e2ea1
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog;
+
+import java.util.List;
+
+/**
+ * Simple Row layout implementation
+ * - also supports weight and horizontal/vertical positioning
+ */
+public class RowLayout extends LayoutManager implements ComponentStartOperation {
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+    public static final int SPACE_BETWEEN = 6;
+    public static final int SPACE_EVENLY = 7;
+    public static final int SPACE_AROUND = 8;
+
+    public static final RowLayout.Companion COMPANION = new RowLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+    float mSpacedBy = 0f;
+
+    public RowLayout(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+        mSpacedBy = spacedBy;
+    }
+
+    public RowLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning, spacedBy);
+    }
+    @Override
+    public String toString() {
+        return "ROW [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "ROW";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        DebugLog.s(() -> "COMPUTE WRAP SIZE in " + this + " (" + mComponentId + ")");
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth, 0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(size.getWidth() + m.getW());
+            size.setHeight(Math.max(size.getHeight(), m.getH()));
+        }
+        if (!mChildrenComponents.isEmpty()) {
+            size.setWidth(size.getWidth()
+                    + (mSpacedBy * (mChildrenComponents.size() - 1)));
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        DebugLog.s(() -> "COMPUTE SIZE in " + this + " (" + mComponentId + ")");
+        float mw = maxWidth;
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, mw, minHeight, maxHeight, measure);
+            ComponentMeasure m = measure.get(child);
+            mw -= m.getW();
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        DebugLog.s(() -> "INTERNAL LAYOUT " + this + " (" + mComponentId + ") children: "
+                + mChildrenComponents.size() + " size (" + selfMeasure.getW()
+                + " x " + selfMeasure.getH() + ")");
+        if (mChildrenComponents.isEmpty()) {
+            DebugLog.e();
+            return;
+        }
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        float childrenWidth = 0f;
+        float childrenHeight = 0f;
+
+        boolean hasWeights = false;
+        float totalWeights = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            if (child instanceof LayoutComponent
+                    && ((LayoutComponent) child).getWidthModifier().hasWeight()) {
+                hasWeights = true;
+                totalWeights += ((LayoutComponent) child).getWidthModifier().getValue();
+            } else {
+                childrenWidth += childMeasure.getW();
+            }
+        }
+
+        // TODO: need to move the weight measuring in the measure function,
+        // currently we'll measure unnecessarily
+        if (hasWeights) {
+            float availableSpace = selfWidth - childrenWidth;
+            for (Component child : mChildrenComponents) {
+                if (child instanceof LayoutComponent
+                        && ((LayoutComponent) child).getWidthModifier().hasWeight()) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    float weight = ((LayoutComponent) child).getWidthModifier().getValue();
+                    childMeasure.setW((weight * availableSpace) / totalWeights);
+                    child.measure(context, childMeasure.getW(),
+                            childMeasure.getW(), childMeasure.getH(), childMeasure.getH(), measure);
+                }
+            }
+        }
+
+        childrenWidth = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            childrenWidth += childMeasure.getW();
+            childrenHeight = Math.max(childrenHeight, childMeasure.getH());
+        }
+        childrenWidth += mSpacedBy * (mChildrenComponents.size() - 1);
+
+        float tx = 0f;
+        float ty = 0f;
+
+        float horizontalGap = 0f;
+        float total = 0f;
+
+        switch (mHorizontalPositioning) {
+            case START:
+                tx = 0f;
+                break;
+            case END:
+                tx = selfWidth - childrenWidth;
+                break;
+            case CENTER:
+                tx = (selfWidth - childrenWidth) / 2f;
+                break;
+            case SPACE_BETWEEN:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size() - 1);
+                break;
+            case SPACE_EVENLY:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size() + 1);
+                tx = horizontalGap;
+                break;
+            case SPACE_AROUND:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size());
+                tx = horizontalGap / 2f;
+                break;
+        }
+
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            switch (mVerticalPositioning) {
+                case TOP:
+                    ty = 0f;
+                    break;
+                case CENTER:
+                    ty = (selfHeight - childMeasure.getH()) / 2f;
+                    break;
+                case BOTTOM:
+                    ty = selfHeight - childMeasure.getH();
+                    break;
+            }
+            childMeasure.setX(tx);
+            childMeasure.setY(ty);
+            childMeasure.setVisibility(child.mVisibility);
+            tx += childMeasure.getW();
+            if (mHorizontalPositioning == SPACE_BETWEEN
+                    || mHorizontalPositioning == SPACE_AROUND
+                    || mHorizontalPositioning == SPACE_EVENLY) {
+                tx += horizontalGap;
+            }
+            tx += mSpacedBy;
+        }
+        DebugLog.e();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "RowLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_ROW;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning, float spacedBy) {
+            buffer.start(Operations.LAYOUT_ROW);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+            buffer.writeFloat(spacedBy);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            float spacedBy = buffer.readFloat();
+            operations.add(new RowLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning, spacedBy));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Row layout implementation, positioning components one"
+                            + " after the other horizontally.\n\n"
+                            + "It supports weight and horizontal/vertical positioning.")
+                    .examplesDimension(400, 100)
+                    .exampleImage("Start", "layout-RowLayout-start-top.png")
+                    .exampleImage("Center", "layout-RowLayout-center-top.png")
+                    .exampleImage("End", "layout-RowLayout-end-top.png")
+                    .exampleImage("SpaceEvenly", "layout-RowLayout-space-evenly-top.png")
+                    .exampleImage("SpaceAround", "layout-RowLayout-space-around-top.png")
+                    .exampleImage("SpaceBetween", "layout-RowLayout-space-between-top.png")
+                    .field(INT, "COMPONENT_ID", "unique id for this component")
+                    .field(INT, "ANIMATION_ID", "id used to match components,"
+                          + " for animation purposes")
+                    .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+                    .possibleValues("START", RowLayout.START)
+                    .possibleValues("CENTER", RowLayout.CENTER)
+                    .possibleValues("END", RowLayout.END)
+                    .possibleValues("SPACE_BETWEEN", RowLayout.SPACE_BETWEEN)
+                    .possibleValues("SPACE_EVENLY", RowLayout.SPACE_EVENLY)
+                    .possibleValues("SPACE_AROUND", RowLayout.SPACE_AROUND)
+                    .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+                    .possibleValues("TOP", RowLayout.TOP)
+                    .possibleValues("CENTER", RowLayout.CENTER)
+                    .possibleValues("BOTTOM", RowLayout.BOTTOM)
+                    .field(FLOAT, "SPACED_BY", "Horizontal spacing between components");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
new file mode 100644
index 0000000..8dc10d5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
@@ -0,0 +1,91 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+
+/**
+ * Encapsulate the result of a measure pass for a component
+ */
+public class ComponentMeasure {
+    int mId = -1;
+    float mX;
+    float mY;
+    float mW;
+    float mH;
+    Component.Visibility mVisibility = Component.Visibility.VISIBLE;
+
+    public void setX(float value) {
+        mX = value;
+    }
+    public void setY(float value) {
+        mY = value;
+    }
+    public void setW(float value) {
+        mW = value;
+    }
+    public void setH(float value) {
+        mH = value;
+    }
+    public float getX() {
+        return mX;
+    }
+    public float getY() {
+        return mY;
+    }
+    public float getW() {
+        return mW;
+    }
+    public float getH() {
+        return mH;
+    }
+
+    public Component.Visibility getVisibility() {
+        return mVisibility;
+    }
+
+    public void setVisibility(Component.Visibility visibility) {
+        mVisibility = visibility;
+    }
+
+    public ComponentMeasure(int id, float x, float y, float w, float h,
+                            Component.Visibility visibility) {
+        this.mId = id;
+        this.mX = x;
+        this.mY = y;
+        this.mW = w;
+        this.mH = h;
+        this.mVisibility = visibility;
+    }
+
+    public ComponentMeasure(int id, float x, float y, float w, float h) {
+        this(id, x, y, w, h, Component.Visibility.VISIBLE);
+    }
+
+    public ComponentMeasure(Component component) {
+        this(component.getComponentId(), component.getX(), component.getY(),
+                component.getWidth(), component.getHeight(),
+                component.mVisibility);
+    }
+
+    public void copyFrom(ComponentMeasure m) {
+        mX = m.mX;
+        mY = m.mY;
+        mW = m.mW;
+        mH = m.mH;
+        mVisibility = m.mVisibility;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
new file mode 100644
index 0000000..d167d9b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
@@ -0,0 +1,45 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * Interface describing the measure/layout contract for components
+ */
+public interface Measurable {
+
+    /**
+     * Measure a component and store the result of the measure in the provided MeasurePass.
+     * This does not apply the measure to the component.
+     */
+    void measure(PaintContext context, float minWidth, float maxWidth,
+                 float minHeight, float maxHeight, MeasurePass measure);
+
+    /**
+     * Apply a given measure to the component
+     */
+    void layout(RemoteContext context, MeasurePass measure);
+
+    /**
+     * Return true if the component needs to be remeasured
+     * @return true if need to remeasured, false otherwise
+     */
+    boolean needsMeasure();
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
new file mode 100644
index 0000000..6801deb
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
@@ -0,0 +1,63 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+
+import java.util.HashMap;
+
+/**
+ * Represents the result of a measure pass on the entire hierarchy
+ * TODO: optimize to use a flat array vs the current hashmap
+ */
+public class MeasurePass {
+    HashMap<Integer, ComponentMeasure> mList = new HashMap<>();
+
+    public void clear() {
+        mList.clear();
+    }
+
+    public void add(ComponentMeasure measure) throws Exception {
+        if (measure.mId == -1) {
+            throw new Exception("Component has no id!");
+        }
+        mList.put(measure.mId, measure);
+    }
+
+    public boolean contains(int id) {
+        return mList.containsKey(id);
+    }
+
+    public ComponentMeasure get(Component c) {
+        if (!mList.containsKey(c.getComponentId())) {
+            ComponentMeasure measure = new ComponentMeasure(c.getComponentId(),
+                    c.getX(), c.getY(), c.getWidth(), c.getHeight());
+            mList.put(c.getComponentId(), measure);
+            return measure;
+        }
+        return mList.get(c.getComponentId());
+    }
+
+    public ComponentMeasure get(int id) {
+        if (!mList.containsKey(id)) {
+            ComponentMeasure measure = new ComponentMeasure(id,
+                    0f, 0f, 0f, 0f, Component.Visibility.GONE);
+            mList.put(id, measure);
+            return measure;
+        }
+        return mList.get(id);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.java
new file mode 100644
index 0000000..b11d8e8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.java
@@ -0,0 +1,44 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+
+/**
+ * Basic data class representing a component size, used during layout computations.
+ */
+public class Size {
+    float mWidth;
+    float mHeight;
+    public Size(float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    public void setWidth(float value) {
+        mWidth = value;
+    }
+
+    public void setHeight(float value) {
+        mHeight = value;
+    }
+
+    public float getWidth() {
+        return mWidth;
+    }
+
+    public float getHeight() {
+        return mHeight;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
new file mode 100644
index 0000000..6f48aee
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
@@ -0,0 +1,146 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Component size-aware background draw
+ */
+public class BackgroundModifierOperation extends DecoratorModifierOperation {
+
+    public static final BackgroundModifierOperation.Companion COMPANION =
+            new BackgroundModifierOperation.Companion();
+
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    float mR;
+    float mG;
+    float mB;
+    float mA;
+    int mShapeType = ShapeType.RECTANGLE;
+
+    public PaintBundle mPaint = new PaintBundle();
+
+    public BackgroundModifierOperation(float x, float y, float width, float height,
+                                       float r, float g, float b, float a,
+                                       int shapeType) {
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mR = r;
+        this.mG = g;
+        this.mB = b;
+        this.mA = a;
+        this.mShapeType = shapeType;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX, mY, mWidth, mHeight, mR, mG, mB, mA, mShapeType);
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "BACKGROUND = [" + mX + ", "
+                + mY + ", " + mWidth + ", " + mHeight
+                + "] color [" + mR + ", " + mG + ", " + mB + ", " + mA
+                + "] shape [" + mShapeType + "]");
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public String toString() {
+        return "BackgroundModifierOperation(" + mWidth + " x " + mHeight + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+
+        @Override
+        public String name() {
+            return "OrigamiBackground";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_BACKGROUND;
+        }
+
+        public void apply(WireBuffer buffer, float x, float y, float width, float height,
+                                 float r, float g, float b, float a, int shapeType) {
+            buffer.start(Operations.MODIFIER_BACKGROUND);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+            buffer.writeFloat(r);
+            buffer.writeFloat(g);
+            buffer.writeFloat(b);
+            buffer.writeFloat(a);
+            // shape type
+            buffer.writeInt(shapeType);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            float r = buffer.readFloat();
+            float g = buffer.readFloat();
+            float b = buffer.readFloat();
+            float a = buffer.readFloat();
+            // shape type
+            int shapeType = buffer.readInt();
+            operations.add(new BackgroundModifierOperation(x, y, width, height,
+                    r, g, b, a, shapeType));
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.savePaint();
+        mPaint.reset();
+        mPaint.setColor(mR, mG, mB, mA);
+        context.applyPaint(mPaint);
+        if (mShapeType == ShapeType.RECTANGLE) {
+            context.drawRect(0f, 0f, mWidth, mHeight);
+        } else if (mShapeType == ShapeType.CIRCLE) {
+            context.drawCircle(mWidth / 2f, mHeight / 2f,
+                    Math.min(mWidth, mHeight) / 2f);
+        }
+        context.restorePaint();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
new file mode 100644
index 0000000..0b9c01b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Component size-aware border draw
+ */
+public class BorderModifierOperation extends DecoratorModifierOperation {
+
+    public static final BorderModifierOperation.Companion COMPANION =
+            new BorderModifierOperation.Companion();
+
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    float mBorderWidth;
+    float mRoundedCorner;
+    float mR;
+    float mG;
+    float mB;
+    float mA;
+    int mShapeType = ShapeType.RECTANGLE;
+
+    public PaintBundle paint = new PaintBundle();
+
+    public BorderModifierOperation(float x, float y, float width, float height,
+                                   float borderWidth, float roundedCorner,
+                                   float r, float g, float b, float a, int shapeType) {
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mBorderWidth = borderWidth;
+        this.mRoundedCorner = roundedCorner;
+        this.mR = r;
+        this.mG = g;
+        this.mB = b;
+        this.mA = a;
+        this.mShapeType = shapeType;
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "BORDER = [" + mX + ", " + mY + ", "
+                + mWidth + ", " + mHeight + "] "
+                + "color [" + mR + ", " + mG + ", " + mB + ", " + mA + "] "
+                + "border [" + mBorderWidth + ", " + mRoundedCorner + "] "
+                + "shape [" + mShapeType + "]");
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX, mY, mWidth, mHeight, mBorderWidth, mRoundedCorner,
+                mR, mG, mB, mA, mShapeType);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public String toString() {
+        return "BorderModifierOperation(" + mX + "," + mY + " - " + mWidth + " x " + mHeight + ") "
+                + "borderWidth(" + mBorderWidth + ") "
+                + "color(" + mR + "," + mG + "," + mB + "," + mA + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        @Override
+        public String name() {
+            return "BorderModifier";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_BORDER;
+        }
+
+        public void apply(WireBuffer buffer, float x, float y, float width, float height,
+                                 float borderWidth, float roundedCorner,
+                                 float r, float g, float b, float a,
+                                 int shapeType) {
+            buffer.start(Operations.MODIFIER_BORDER);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+            buffer.writeFloat(borderWidth);
+            buffer.writeFloat(roundedCorner);
+            buffer.writeFloat(r);
+            buffer.writeFloat(g);
+            buffer.writeFloat(b);
+            buffer.writeFloat(a);
+            // shape type
+            buffer.writeInt(shapeType);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            float bw = buffer.readFloat();
+            float rc = buffer.readFloat();
+            float r = buffer.readFloat();
+            float g = buffer.readFloat();
+            float b = buffer.readFloat();
+            float a = buffer.readFloat();
+            // shape type
+            int shapeType = buffer.readInt();
+            operations.add(new BorderModifierOperation(x, y, width, height, bw,
+                    rc, r, g, b, a, shapeType));
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.savePaint();
+        paint.reset();
+        paint.setColor(mR, mG, mB, mA);
+        paint.setStrokeWidth(mBorderWidth);
+        paint.setStyle(PaintBundle.STYLE_STROKE);
+        context.applyPaint(paint);
+        if (mShapeType == ShapeType.RECTANGLE) {
+            context.drawRect(0f, 0f, mWidth, mHeight);
+        } else {
+            float size = mRoundedCorner;
+            if (mShapeType == ShapeType.CIRCLE) {
+                size = Math.min(mWidth, mHeight) / 2f;
+            }
+            context.drawRoundRect(0f, 0f, mWidth, mHeight, size, size);
+        }
+        context.restorePaint();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
new file mode 100644
index 0000000..30357af
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
@@ -0,0 +1,88 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Support modifier clip with a rectangle
+ */
+public class ClipRectModifierOperation extends DecoratorModifierOperation {
+
+    public static final ClipRectModifierOperation.Companion COMPANION =
+            new ClipRectModifierOperation.Companion();
+
+    float mWidth;
+    float mHeight;
+
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipRect(0f, 0f, mWidth, mHeight);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(
+                indent, "CLIP_RECT = [" + mWidth + ", " + mHeight + "]");
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        @Override
+        public String name() {
+            return "ClipRectModifier";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_CLIP_RECT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MODIFIER_CLIP_RECT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new ClipRectModifierOperation());
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
new file mode 100644
index 0000000..2ef0b9d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
@@ -0,0 +1,109 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.ArrayList;
+
+/**
+ * Maintain a list of modifiers
+ */
+public class ComponentModifiers extends PaintOperation implements DecoratorComponent {
+    ArrayList<ModifierOperation> mList = new ArrayList<>();
+
+    public ArrayList<ModifierOperation> getList() {
+        return mList;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        // nothing
+    }
+
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "MODIFIERS");
+        for (ModifierOperation m : mList) {
+            m.serializeToString(indent + 1, serializer);
+        }
+    }
+
+    public void add(ModifierOperation operation) {
+        mList.add(operation);
+    }
+
+    public int size() {
+        return mList.size();
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        float tx = 0f;
+        float ty = 0f;
+        for (ModifierOperation op : mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                context.translate(pop.getLeft(), pop.getTop());
+                tx += pop.getLeft();
+                ty += pop.getTop();
+            }
+            if (op instanceof MatrixSave || op instanceof MatrixRestore) {
+                continue;
+            }
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+        // Back out the translates created by paddings
+        // TODO: we should be able to get rid of this when drawing the content of a component
+        context.translate(-tx, -ty);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        float w = width;
+        float h = height;
+        for (ModifierOperation op : mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                w -= pop.getLeft() + pop.getRight();
+                h -= pop.getTop() + pop.getBottom();
+            }
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).layout(context, w, h);
+            }
+        }
+    }
+
+    public void addAll(ArrayList<ModifierOperation> operations) {
+        mList.addAll(operations);
+    }
+
+    public void onClick(float x, float y) {
+        for (ModifierOperation op : mList) {
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).onClick(x, y);
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java
new file mode 100644
index 0000000..bf9b27b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+
+/**
+ * Represents a decorator modifier (lightweight component), ie a modifier
+ * that impacts the visual output (background, border...)
+ */
+public abstract class DecoratorModifierOperation extends PaintOperation
+        implements ModifierOperation, DecoratorComponent {
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
new file mode 100644
index 0000000..04e9431
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
@@ -0,0 +1,159 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Base class for dimension modifiers
+ */
+public class DimensionModifierOperation implements ModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(0, "DIMENSION");
+
+    public enum Type {
+        EXACT, FILL, WRAP, WEIGHT, INTRINSIC_MIN, INTRINSIC_MAX;
+
+        static Type fromInt(int value) {
+            switch (value) {
+                case 0: return EXACT;
+                case 1: return FILL;
+                case 2: return WRAP;
+                case 3: return WEIGHT;
+                case 4: return INTRINSIC_MIN;
+                case 5: return INTRINSIC_MAX;
+            }
+            return EXACT;
+        }
+    }
+
+    Type mType = Type.EXACT;
+    float mValue = Float.NaN;
+
+    public DimensionModifierOperation(Type type, float value) {
+        mType = type;
+        mValue = value;
+    }
+
+    public DimensionModifierOperation(Type type) {
+        this(type, Float.NaN);
+    }
+
+    public DimensionModifierOperation(float value) {
+        this(Type.EXACT, value);
+    }
+
+
+    public boolean hasWeight() {
+        return mType == Type.WEIGHT;
+    }
+
+    public boolean isWrap() {
+        return mType == Type.WRAP;
+    }
+
+    public boolean isFill() {
+        return mType == Type.FILL;
+    }
+
+    public Type getType() {
+        return mType;
+    }
+
+    public float getValue() {
+        return mValue;
+    }
+
+    public void setValue(float value) {
+        this.mValue = value;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mType.ordinal(), mValue);
+    }
+
+    public String serializedName() {
+        return "DIMENSION";
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        if (mType == Type.EXACT) {
+            serializer.append(indent, serializedName() + " = " + mValue);
+        }
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public String toString() {
+        return "DimensionModifierOperation(" + mValue + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        int mOperation;
+        String mName;
+
+        public Companion(int operation, String name) {
+            mOperation = operation;
+            mName = name;
+        }
+
+        @Override
+        public String name() {
+            return mName;
+        }
+
+        @Override
+        public int id() {
+            return mOperation;
+        }
+
+        public void apply(WireBuffer buffer, int type, float value) {
+            buffer.start(mOperation);
+            buffer.writeInt(type);
+            buffer.writeFloat(value);
+        }
+
+        public Operation construct(Type type, float value) {
+            return null;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            Type type = Type.fromInt(buffer.readInt());
+            float value = buffer.readFloat();
+            Operation op = construct(type, value);
+            operations.add(op);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
new file mode 100644
index 0000000..81173c3
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
@@ -0,0 +1,55 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+
+/**
+ * Set the height dimension on a component
+ */
+public class HeightModifierOperation extends DimensionModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(Operations.MODIFIER_HEIGHT, "WIDTH") {
+                @Override
+                public Operation construct(DimensionModifierOperation.Type type, float value) {
+                    return new HeightModifierOperation(type, value);
+                }
+            };
+
+    public HeightModifierOperation(Type type, float value) {
+        super(type, value);
+    }
+
+    public HeightModifierOperation(Type type) {
+        super(type);
+    }
+
+    public HeightModifierOperation(float value) {
+        super(value);
+    }
+
+    @Override
+    public String toString() {
+        return "Height(" + mValue + ")";
+    }
+
+    @Override
+    public String serializedName() {
+        return "HEIGHT";
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
similarity index 60%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
rename to core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
index 14b9e7f..5299719 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
@@ -13,15 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.wm.shell.windowdecor.common
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
 
-/** A callback to be invoked when a Task's window decor element is clicked. */
-fun interface OnTaskActionClickListener {
-    /**
-     * Called when a task's decor element has been clicked.
-     *
-     * @param taskId the id of the task.
-     * @param tag a readable identifier for the element.
-     */
-    fun onClick(taskId: Int, tag: String)
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+/**
+ * Represents a modifier
+ */
+public interface ModifierOperation extends Operation {
+    void serializeToString(int indent, StringSerializer serializer);
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
new file mode 100644
index 0000000..5ea6a97
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
@@ -0,0 +1,135 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Represents a padding modifier.
+ * Padding modifiers can be chained and will impact following modifiers.
+ */
+public class PaddingModifierOperation implements ModifierOperation {
+
+    public static final PaddingModifierOperation.Companion COMPANION =
+            new PaddingModifierOperation.Companion();
+
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public PaddingModifierOperation(float left, float top, float right, float bottom) {
+        this.mLeft = left;
+        this.mTop = top;
+        this.mRight = right;
+        this.mBottom = bottom;
+    }
+
+    public float getLeft() {
+        return mLeft;
+    }
+
+    public float getTop() {
+        return mTop;
+    }
+
+    public float getRight() {
+        return mRight;
+    }
+
+    public float getBottom() {
+        return mBottom;
+    }
+
+    public void setLeft(float left) {
+        this.mLeft = left;
+    }
+
+    public void setTop(float top) {
+        this.mTop = top;
+    }
+
+    public void setRight(float right) {
+        this.mRight = right;
+    }
+
+    public void setBottom(float bottom) {
+        this.mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "PADDING = [" + mLeft + ", " + mTop + ", "
+                + mRight + ", " + mBottom + "]");
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public String toString() {
+        return "PaddingModifierOperation(" + mLeft + ", " + mTop
+                + ", " + mRight + ", " + mBottom + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+        @Override
+        public String name() {
+            return "PaddingModifierOperation";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_PADDING;
+        }
+
+        public void apply(WireBuffer buffer,
+                                 float left, float top, float right, float bottom) {
+            buffer.start(Operations.MODIFIER_PADDING);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float left = buffer.readFloat();
+            float top = buffer.readFloat();
+            float right = buffer.readFloat();
+            float bottom = buffer.readFloat();
+            operations.add(new PaddingModifierOperation(left, top, right, bottom));
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
new file mode 100644
index 0000000..9c57c6a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
@@ -0,0 +1,78 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.DrawBase4;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+/**
+ * Support clip with a rectangle
+ */
+public class RoundedClipRectModifierOperation extends DrawBase4
+        implements ModifierOperation, DecoratorComponent {
+
+    public static final Companion COMPANION =
+            new Companion(Operations.MODIFIER_ROUNDED_CLIP_RECT) {
+                @Override
+                public Operation construct(float x1,
+                                           float y1,
+                                           float x2,
+                                           float y2) {
+                    return new RoundedClipRectModifierOperation(x1, y1, x2, y2);
+                }
+            };
+    float mWidth;
+    float mHeight;
+
+
+    public RoundedClipRectModifierOperation(
+            float topStart,
+            float topEnd,
+            float bottomStart,
+            float bottomEnd) {
+        super(topStart, topEnd, bottomStart, bottomEnd);
+        mName = "ModifierRoundedClipRect";
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.roundedClipRect(mWidth, mHeight, mX1, mY1, mX2, mY2);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(
+                indent, "ROUND_CLIP = [" + mWidth + ", " + mHeight
+                        + ", " + mX1 + ", " + mY1
+                        + ", " + mX2 + ", " + mY2 + "]");
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java
similarity index 61%
copy from packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
copy to core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java
index 9a5c77a..e425b4e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -13,9 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
 
-package com.android.systemui.shared.education
-
-enum class GestureType {
-    BACK_GESTURE,
+/**
+ * Known shapes, used for modifiers (clip/background/border)
+ */
+public class ShapeType {
+    public static int RECTANGLE = 0;
+    public static int CIRCLE = 1;
+    public static int ROUNDED_RECTANGLE = 2;
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
new file mode 100644
index 0000000..c46c8d7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
@@ -0,0 +1,55 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+
+/**
+ * Set the width dimension on a component
+ */
+public class WidthModifierOperation extends DimensionModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(Operations.MODIFIER_WIDTH, "WIDTH") {
+                @Override
+                public Operation construct(DimensionModifierOperation.Type type, float value) {
+                    return new WidthModifierOperation(type, value);
+                }
+            };
+
+    public WidthModifierOperation(Type type, float value) {
+        super(type, value);
+    }
+
+    public WidthModifierOperation(Type type) {
+        super(type);
+    }
+
+    public WidthModifierOperation(float value) {
+        super(value);
+    }
+
+    @Override
+    public String toString() {
+        return "Width(" + mValue + ")";
+    }
+
+    @Override
+    public String serializedName() {
+        return "WIDTH";
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
new file mode 100644
index 0000000..7ccf7f4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
@@ -0,0 +1,127 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.utils;
+
+import java.util.ArrayList;
+
+/**
+ * Internal utility debug class
+ */
+public class DebugLog {
+
+    public static final boolean DEBUG_LAYOUT_ON = false;
+
+    public static class Node {
+        public Node parent;
+        public String name;
+        public String endString;
+        public ArrayList<Node> list = new ArrayList<>();
+
+        public Node(Node parent, String name) {
+            this.parent = parent;
+            this.name = name;
+            this.endString = name + " DONE";
+            if (parent != null) {
+                parent.add(this);
+            }
+        }
+
+        public void add(Node node) {
+            list.add(node);
+        }
+    }
+
+    public static class LogNode extends Node {
+        public LogNode(Node parent, String name) {
+            super(parent, name);
+        }
+    }
+
+    public static Node node = new Node(null, "Root");
+    public static Node currentNode = node;
+
+    public static void clear() {
+        node = new Node(null, "Root");
+        currentNode = node;
+    }
+
+    public static void s(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            currentNode = new Node(currentNode, valueSupplier.getString());
+        }
+    }
+
+    public static void log(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            new LogNode(currentNode, valueSupplier.getString());
+        }
+    }
+
+    public static void e() {
+        if (DEBUG_LAYOUT_ON) {
+            if (currentNode.parent != null) {
+                currentNode = currentNode.parent;
+            } else {
+                currentNode = node;
+            }
+        }
+    }
+
+    public static void e(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            currentNode.endString = valueSupplier.getString();
+            if (currentNode.parent != null) {
+                currentNode = currentNode.parent;
+            } else {
+                currentNode = node;
+            }
+        }
+    }
+
+    public static void printNode(int indent, Node node, StringBuilder builder) {
+        if (DEBUG_LAYOUT_ON) {
+            StringBuilder indentationBuilder = new StringBuilder();
+            for (int i = 0; i < indent; i++) {
+                indentationBuilder.append("| ");
+            }
+            String indentation = indentationBuilder.toString();
+
+            if (node.list.size() > 0) {
+                builder.append(indentation).append(node.name).append("\n");
+                for (Node c : node.list) {
+                    printNode(indent + 1, c, builder);
+                }
+                builder.append(indentation).append(node.endString).append("\n");
+            } else {
+                if (node instanceof LogNode) {
+                    builder.append(indentation).append("     ").append(node.name).append("\n");
+                } else {
+                    builder.append(indentation).append("-- ").append(node.name)
+                            .append(" : ").append(node.endString).append("\n");
+                }
+            }
+        }
+    }
+
+    public static void display() {
+        if (DEBUG_LAYOUT_ON) {
+            StringBuilder builder = new StringBuilder();
+            printNode(0, node, builder);
+            System.out.println("\n" + builder.toString());
+        }
+    }
+}
+
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
similarity index 62%
copy from packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
copy to core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
index 9a5c77a..79ef16b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -13,9 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.internal.widget.remotecompose.core.operations.layout.utils;
 
-package com.android.systemui.shared.education
-
-enum class GestureType {
-    BACK_GESTURE,
+/**
+ * Basic interface for a lambda (used for logging)
+ */
+public interface StringValueSupplier {
+    /**
+     * returns a string value
+     * @return a string
+     */
+    String getString();
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
index a7d0ac6..8186192 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
@@ -695,6 +695,29 @@
     }
 
     /**
+     * Set the color based the R,G,B,A values
+     * @param r red (0 to 255)
+     * @param g green (0 to 255)
+     * @param b blue (0 to 255)
+     * @param a alpha (0 to 255)
+     */
+    public void setColor(int r, int g, int b, int a) {
+        int color = (a << 24) | (r << 16) | (g << 8) | b;
+        setColor(color);
+    }
+
+    /**
+     * Set the color based the R,G,B,A values
+     * @param r red (0.0 to 1.0)
+     * @param g green (0.0 to 1.0)
+     * @param b blue (0.0 to 1.0)
+     * @param a alpha (0.0 to 1.0)
+     */
+    public void setColor(float r, float g, float b, float a) {
+        setColor((int) r * 255, (int) g * 255, (int) b * 255, (int) a * 255);
+    }
+
+    /**
      * Set the Color based on ID
      * @param color
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
index 23c3ec5..b2d714e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
@@ -54,7 +54,7 @@
     /**
      * Put a item in the map
      *
-     * @param key   item'values key
+     * @param key item's key
      * @param value item's value
      * @return old value if exist
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
index 221014c..606dc78 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
@@ -53,7 +53,7 @@
     /**
      * Put a item in the map
      *
-     * @param key   item'values key
+     * @param key item's key
      * @param value item's value
      * @return old value if exist
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
index 4c1389c..a4fce80 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
@@ -400,8 +400,7 @@
             -1, // no op
             2, 2, 2, 2, 2, // + - * / %
             2, 2, 2, 2, 2, 2, 2, 2, 2, //<<, >> , >>> , | , &, ^, min max
-            1, 1, 1, 1, 1, 1,   // neg, abs, ++, -- , not , sign
-
+            1, 1, 1, 1, 1, 1,  // neg, abs, ++, -- , not , sign
             3, 3, 3, // clamp, ifElse, mad,
             0, 0, 0 // mad, ?:,
             // a[0],a[1],a[2]
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java
new file mode 100644
index 0000000..fb90781
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.utilities;
+
+/**
+ * Utility serializer maintaining an indent buffer
+ */
+public class StringSerializer {
+    StringBuffer mBuffer = new StringBuffer();
+    String mIndentBuffer = "                                                                      ";
+
+    /**
+     * Append some content to the current buffer
+     * @param indent the indentation level to use
+     * @param content content to append
+     */
+    public void append(int indent, String content) {
+        String indentation = mIndentBuffer.substring(0, indent);
+        mBuffer.append(indentation);
+        mBuffer.append(indentation);
+        mBuffer.append(content);
+        mBuffer.append("\n");
+    }
+
+    /**
+     * Reset the buffer
+     */
+    public void reset() {
+        mBuffer = new StringBuffer();
+    }
+
+    /**
+     * Return a string representation of the buffer
+     * @return string representation
+     */
+    public String toString() {
+        return mBuffer.toString();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
index 693deaf..50a7d59 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
@@ -18,7 +18,7 @@
 /**
  * Provides and interface to create easing functions
  */
-public class GeneralEasing extends  Easing{
+public class GeneralEasing extends Easing{
     float[] mEasingData = new float[0];
     Easing mEasingCurve = new CubicEasing(CUBIC_STANDARD);
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
index a42c584..65a337e 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
@@ -18,6 +18,7 @@
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.RemoteComposeBuffer;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
 
 import java.io.InputStream;
 
@@ -113,5 +114,13 @@
         return mDocument.getNamedColors();
     }
 
+    /**
+     * Return a component associated with id
+     * @param id the component id
+     * @return the corresponding component or null if not found
+     */
+    public Component getComponent(int id) {
+        return mDocument.getComponent(id);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index 39a770a..e01dd17 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -65,6 +65,21 @@
         this.mCanvas = canvas;
     }
 
+    @Override
+    public void save() {
+        mCanvas.save();
+    }
+
+    @Override
+    public void saveLayer(float x, float y, float width, float height) {
+        mCanvas.saveLayer(x, y, x + width, y + height, mPaint);
+    }
+
+    @Override
+    public void restore() {
+        mCanvas.restore();
+    }
+
     /**
      * Draw an image onto the canvas
      *
@@ -613,6 +628,19 @@
     }
 
     @Override
+    public void roundedClipRect(float width, float height,
+                                float topStart, float topEnd,
+                                float bottomStart, float bottomEnd) {
+        Path roundedPath = new Path();
+        float[] radii = new float[] { topStart, topStart,
+                topEnd, topEnd, bottomEnd, bottomEnd, bottomStart, bottomStart};
+
+        roundedPath.addRoundRect(0f, 0f, width, height,
+                radii, android.graphics.Path.Direction.CW);
+        mCanvas.clipPath(roundedPath);
+    }
+
+    @Override
     public void clipPath(int pathId, int regionOp) {
         Path path = getPath(pathId, 0, 1);
         if (regionOp == ClipPath.DIFFERENCE) {
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
index a2f79cc..0d7f97a 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -215,6 +215,7 @@
         }
         int w = measureDimension(widthMeasureSpec, mDocument.getWidth());
         int h = measureDimension(heightMeasureSpec, mDocument.getHeight());
+        mDocument.getDocument().invalidateMeasure();
 
         if (!USE_VIEW_AREA_CLICK) {
             if (mDocument.getDocument().getContentSizing() == RootContentBehavior.SIZING_SCALE) {
@@ -235,6 +236,8 @@
         if (mDocument == null) {
             return;
         }
+        mARContext.setAnimationEnabled(true);
+        mARContext.currentTime = System.currentTimeMillis();
         mARContext.setDebug(mDebug);
         mARContext.useCanvas(canvas);
         mARContext.mWidth = getWidth();
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index ca984c0..2abdd57 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -258,6 +258,7 @@
                 "com_android_internal_content_om_OverlayManagerImpl.cpp",
                 "com_android_internal_net_NetworkUtilsInternal.cpp",
                 "com_android_internal_os_ClassLoaderFactory.cpp",
+                "com_android_internal_os_DebugStore.cpp",
                 "com_android_internal_os_FuseAppLoop.cpp",
                 "com_android_internal_os_KernelAllocationStats.cpp",
                 "com_android_internal_os_KernelCpuBpfTracking.cpp",
@@ -315,6 +316,7 @@
                 "libcrypto",
                 "libcutils",
                 "libdebuggerd_client",
+                "libdebugstore_cxx",
                 "libutils",
                 "libbinder",
                 "libbinderdebug",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index ed59327..03b5143a 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -202,6 +202,7 @@
 extern int register_com_android_internal_content_om_OverlayManagerImpl(JNIEnv* env);
 extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
 extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env);
+extern int register_com_android_internal_os_DebugStore(JNIEnv* env);
 extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
 extern int register_com_android_internal_os_KernelAllocationStats(JNIEnv* env);
 extern int register_com_android_internal_os_KernelCpuBpfTracking(JNIEnv* env);
@@ -1599,6 +1600,7 @@
         REG_JNI(register_com_android_internal_content_om_OverlayManagerImpl),
         REG_JNI(register_com_android_internal_net_NetworkUtilsInternal),
         REG_JNI(register_com_android_internal_os_ClassLoaderFactory),
+        REG_JNI(register_com_android_internal_os_DebugStore),
         REG_JNI(register_com_android_internal_os_LongArrayMultiStateCounter),
         REG_JNI(register_com_android_internal_os_LongMultiStateCounter),
         REG_JNI(register_com_android_internal_os_Zygote),
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 9ce7658..0f53164 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -22,6 +22,7 @@
 #include <android/graphics/properties.h>
 #include <android/graphics/region.h>
 #include <android/gui/BnWindowInfosReportedListener.h>
+#include <android/gui/EdgeExtensionParameters.h>
 #include <android/gui/JankData.h>
 #include <android/hardware/display/IDeviceProductInfoConstants.h>
 #include <android/os/IInputConstants.h>
@@ -799,6 +800,20 @@
     transaction->setStretchEffect(ctrl, stretch);
 }
 
+static void nativeSetEdgeExtensionEffect(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                         jlong nativeObj, jboolean leftEdge, jboolean rightEdge,
+                                         jboolean topEdge, jboolean bottomEdge) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    auto* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObj);
+
+    auto effect = gui::EdgeExtensionParameters();
+    effect.extendLeft = leftEdge;
+    effect.extendRight = rightEdge;
+    effect.extendTop = topEdge;
+    effect.extendBottom = bottomEdge;
+    transaction->setEdgeExtensionEffect(ctrl, effect);
+}
+
 static void nativeSetFlags(JNIEnv* env, jclass clazz, jlong transactionObj,
         jlong nativeObject, jint flags, jint mask) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -2340,6 +2355,8 @@
             (void*)nativeSetBlurRegions },
     {"nativeSetStretchEffect", "(JJFFFFFFFFFF)V",
             (void*) nativeSetStretchEffect },
+    {"nativeSetEdgeExtensionEffect", "(JJZZZZ)V",
+            (void*) nativeSetEdgeExtensionEffect },
     {"nativeSetShadowRadius", "(JJF)V",
             (void*)nativeSetShadowRadius },
     {"nativeSetFrameRate", "(JJFII)V",
diff --git a/core/jni/com_android_internal_os_DebugStore.cpp b/core/jni/com_android_internal_os_DebugStore.cpp
new file mode 100644
index 0000000..874d6ea
--- /dev/null
+++ b/core/jni/com_android_internal_os_DebugStore.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#include <debugstore/debugstore_cxx_bridge.rs.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+
+#include <iterator>
+#include <sstream>
+#include <vector>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jmethodID mGet;
+    jmethodID mSize;
+} gListClassInfo;
+
+static std::vector<std::string> list_to_vector(JNIEnv* env, jobject jList) {
+    std::vector<std::string> vec;
+    jint size = env->CallIntMethod(jList, gListClassInfo.mSize);
+    if (size % 2 != 0) {
+        std::ostringstream oss;
+
+        std::copy(vec.begin(), vec.end(), std::ostream_iterator<std::string>(oss, ", "));
+        ALOGW("DebugStore list size is odd: %d, elements: %s", size, oss.str().c_str());
+
+        return vec;
+    }
+
+    vec.reserve(size);
+
+    for (jint i = 0; i < size; i++) {
+        ScopedLocalRef<jstring> jEntry(env,
+                                       reinterpret_cast<jstring>(
+                                               env->CallObjectMethod(jList, gListClassInfo.mGet,
+                                                                     i)));
+        ScopedUtfChars cEntry(env, jEntry.get());
+        vec.emplace_back(cEntry.c_str());
+    }
+    return vec;
+}
+
+static void com_android_internal_os_DebugStore_endEvent(JNIEnv* env, jclass clazz, jlong eventId,
+                                                        jobject jAttributeList) {
+    auto attributes = list_to_vector(env, jAttributeList);
+    debugstore::debug_store_end(static_cast<uint64_t>(eventId), attributes);
+}
+
+static jlong com_android_internal_os_DebugStore_beginEvent(JNIEnv* env, jclass clazz,
+                                                           jstring jeventName,
+                                                           jobject jAttributeList) {
+    ScopedUtfChars eventName(env, jeventName);
+    auto attributes = list_to_vector(env, jAttributeList);
+    jlong eventId =
+            static_cast<jlong>(debugstore::debug_store_begin(eventName.c_str(), attributes));
+    return eventId;
+}
+
+static void com_android_internal_os_DebugStore_recordEvent(JNIEnv* env, jclass clazz,
+                                                           jstring jeventName,
+                                                           jobject jAttributeList) {
+    ScopedUtfChars eventName(env, jeventName);
+    auto attributes = list_to_vector(env, jAttributeList);
+    debugstore::debug_store_record(eventName.c_str(), attributes);
+}
+
+static const JNINativeMethod gDebugStoreMethods[] = {
+        /* name, signature, funcPtr */
+        {"beginEventNative", "(Ljava/lang/String;Ljava/util/List;)J",
+         (void*)com_android_internal_os_DebugStore_beginEvent},
+        {"endEventNative", "(JLjava/util/List;)V",
+         (void*)com_android_internal_os_DebugStore_endEvent},
+        {"recordEventNative", "(Ljava/lang/String;Ljava/util/List;)V",
+         (void*)com_android_internal_os_DebugStore_recordEvent},
+};
+
+int register_com_android_internal_os_DebugStore(JNIEnv* env) {
+    int res = RegisterMethodsOrDie(env, "com/android/internal/os/DebugStore", gDebugStoreMethods,
+                                   NELEM(gDebugStoreMethods));
+    jclass listClass = FindClassOrDie(env, "java/util/List");
+    gListClassInfo.mGet = GetMethodIDOrDie(env, listClass, "get", "(I)Ljava/lang/Object;");
+    gListClassInfo.mSize = GetMethodIDOrDie(env, listClass, "size", "()I");
+
+    return res;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index e5ced25..e795e809 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -69,6 +69,7 @@
         // 0 = no, 1 = yes
         optional SettingProto window_orientation_listener_log = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto show_key_presses = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto touchpad_visualizer = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional DevOptions developer_options = 7;
 
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 9207aa8..e900eb2 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -164,6 +164,7 @@
         "com.android.window.flags.window-aconfig",
         "android.permission.flags-aconfig",
         "android.os.flags-aconfig",
+        "android.media.tv.flags-aconfig",
     ],
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f3dac23..50727a2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5609,7 +5609,8 @@
          @hide
     -->
     <permission android:name="android.permission.ALWAYS_BOUND_TV_INPUT"
-        android:protectionLevel="signature|privileged|vendorPrivileged" />
+        android:protectionLevel="signature|privileged|vendorPrivileged"
+        android:featureFlag="android.media.tv.flags.tis_always_bound_permission"/>
 
     <!-- Must be required by a {@link android.media.tv.interactive.TvInteractiveAppService}
          to ensure that only the system can bind to it.
@@ -6107,9 +6108,8 @@
                 android:description="@string/permdesc_deliverCompanionMessages"
                 android:protectionLevel="normal" />
 
-    <!-- @hide @FlaggedApi("android.companion.flags.companion_transport_apis")
-         Allows an application to send and receive messages via CDM transports.
-    -->
+    <!-- Allows an application to send and receive messages via CDM transports.
+         @hide -->
     <permission android:name="android.permission.USE_COMPANION_TRANSPORTS"
         android:protectionLevel="signature" />
 
diff --git a/core/res/TEST_MAPPING b/core/res/TEST_MAPPING
index 4d09076..0e01a2a 100644
--- a/core/res/TEST_MAPPING
+++ b/core/res/TEST_MAPPING
@@ -11,5 +11,22 @@
                 }
             ]
         }
+    ],
+    // v2/sysui/suite/test-mapping-sysui-screenshot-test
+    "sysui-screenshot-test": [
+        {
+        "name": "SystemUIGoogleScreenshotTests",
+        "options": [
+            {
+            "exclude-annotation": "org.junit.Ignore"
+            },
+            {
+            "exclude-annotation": "androidx.test.filters.FlakyTest"
+            },
+            {
+            "exclude-annotation": "android.platform.test.annotations.Postsubmit"
+            }
+        ]
+        }
     ]
-}
\ No newline at end of file
+}
diff --git a/core/res/res/drawable/ic_call_answer_video.xml b/core/res/res/drawable/ic_call_answer_video.xml
index 77c8892..79af247 100644
--- a/core/res/res/drawable/ic_call_answer_video.xml
+++ b/core/res/res/drawable/ic_call_answer_video.xml
@@ -16,8 +16,8 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="24dp"
     android:height="24dp"
-    android:viewportWidth="20"
-    android:viewportHeight="20"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
     android:tint="?android:attr/colorControlNormal"
     android:autoMirrored="true">
     <path
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 495af5b..af0272e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3961,6 +3961,9 @@
          flag does not exist -->
     <bool name="config_magnification_always_on_enabled">true</bool>
 
+    <!-- Whether to keep fullscreen magnification zoom level when context changes. -->
+    <bool name="config_magnification_keep_zoom_level_when_context_changed">false</bool>
+
     <!-- If true, the display will be shifted around in ambient mode. -->
     <bool name="config_enableBurnInProtection">false</bool>
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index e94db2d..9104379 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6496,10 +6496,14 @@
     <string name="screen_not_shared_sensitive_content">App content hidden from screen share for security</string>
 
     <!-- Satellite related messages -->
-    <!-- Notification title when satellite service is connected. -->
+    <!-- Notification title when satellite service is auto connected. -->
     <string name="satellite_notification_title">Auto connected to satellite</string>
-    <!-- Notification summary when satellite service is connected. [CHAR LIMIT=NONE] -->
+    <!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] -->
     <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string>
+    <!-- Notification title when satellite service can be manually enabled. -->
+    <string name="satellite_notification_manual_title">Use satellite messaging?</string>
+    <!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] -->
+    <string name="satellite_notification_manual_summary">Send and receive messages without a mobile or Wi-Fi network</string>
     <!-- Invoke "What to expect" dialog of messaging application -->
     <string name="satellite_notification_open_message">Open Messages</string>
     <!-- Invoke Satellite setting activity of Settings -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8734b44..8f4018f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4779,6 +4779,7 @@
 
   <java-symbol type="bool" name="config_magnification_area" />
   <java-symbol type="bool" name="config_magnification_always_on_enabled" />
+  <java-symbol type="bool" name="config_magnification_keep_zoom_level_when_context_changed" />
 
   <java-symbol type="bool" name="config_trackerAppNeedsPermissions"/>
   <!-- FullScreenMagnification thumbnail -->
@@ -5499,6 +5500,8 @@
   <!-- System notification for satellite service -->
   <java-symbol type="string" name="satellite_notification_title" />
   <java-symbol type="string" name="satellite_notification_summary" />
+  <java-symbol type="string" name="satellite_notification_manual_title" />
+  <java-symbol type="string" name="satellite_notification_manual_summary" />
   <java-symbol type="string" name="satellite_notification_open_message" />
   <java-symbol type="string" name="satellite_notification_how_it_works" />
   <java-symbol type="drawable" name="ic_satellite_alt_24px" />
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index cd6abdd..b5ee130 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -375,20 +375,4 @@
             PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
         assertEquals(n1, "cache_key.bluetooth.get_state");
     }
-
-    @Test
-    public void testOnTrimMemory() {
-        TestCache cache = new TestCache(MODULE, "trimMemoryTest");
-        // The cache is not active until it has been invalidated once.
-        cache.invalidateCache();
-        // Populate the cache with six entries.
-        for (int i = 0; i < 6; i++) {
-            cache.query(i);
-        }
-        // The maximum number of entries in TestCache is 4, so even though six entries were
-        // created, only four are retained.
-        assertEquals(4, cache.size());
-        PropertyInvalidatedCache.onTrimMemory();
-        assertEquals(0, cache.size());
-    }
 }
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index bd9c4b8..519f23b 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -28,6 +28,7 @@
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Printer;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -43,10 +44,13 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Vector;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Phaser;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -400,4 +404,138 @@
         }
         assertFalse(allowed);
     }
+
+    /** Dumpsys information about a single database. */
+
+    /**
+     * Collect and parse dumpsys output.  This is not a full parser.  It is only enough to support
+     * the unit tests.
+     */
+    private static class Dumpsys {
+        // Regular expressions for parsing the output.  Reportedly, regular expressions are
+        // expensive, so these are created only if a dumpsys object is created.
+        private static final Object sLock = new Object();
+        static Pattern mPool;
+        static Pattern mConnection;
+        static Pattern mEntry;
+        static Pattern mSingleWord;
+        static Pattern mNone;
+
+        // The raw strings read from dumpsys.  Once loaded, this list never changes.
+        final ArrayList<String> mRaw = new ArrayList<>();
+
+        // Parsed dumpsys.  This contains only the bits that are being tested.
+        static class Connection {
+            ArrayList<String> mRecent = new ArrayList<>();
+            ArrayList<String> mLong = new ArrayList<>();
+        }
+        static class Database {
+            String mPath;
+            ArrayList<Connection> mConnection = new ArrayList<>();
+        }
+        ArrayList<Database> mDatabase;
+        ArrayList<String> mConcurrent;
+
+        Dumpsys() {
+            SQLiteDebug.dump(
+                new Printer() { public void println(String x) { mRaw.add(x); } },
+                new String[0]);
+            parse();
+        }
+
+        /** Parse the raw text. Return true if no errors were detected. */
+        boolean parse() {
+            initialize();
+
+            // Reset the parsed information.  This method may be called repeatedly.
+            mDatabase = new ArrayList<>();
+            mConcurrent = new ArrayList<>();
+
+            Database current = null;
+            Connection connection = null;
+            Matcher matcher;
+            for (int i = 0; i < mRaw.size(); i++) {
+                final String line = mRaw.get(i);
+                matcher = mPool.matcher(line);
+                if (matcher.lookingAt()) {
+                    current = new Database();
+                    mDatabase.add(current);
+                    current.mPath = matcher.group(1);
+                    continue;
+                }
+                matcher = mConnection.matcher(line);
+                if (matcher.lookingAt()) {
+                    connection = new Connection();
+                    current.mConnection.add(connection);
+                    continue;
+                }
+
+                if (line.contains("Most recently executed operations")) {
+                    i += readTable(connection.mRecent, i, mEntry);
+                    continue;
+                }
+
+                if (line.contains("Operations exceeding 2000ms")) {
+                    i += readTable(connection.mLong, i, mEntry);
+                    continue;
+                }
+                if (line.contains("Concurrently opened database files")) {
+                    i += readTable(mConcurrent, i, mSingleWord);
+                    continue;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Read a series of lines following a header.  Return the number of lines read.  The input
+         * line number is the number of the header.
+         */
+        private int readTable(List<String> s, int header, Pattern p) {
+            // Special case: if the first line is "<none>" then there are no more lines to the
+            // table.
+            if (lookingAt(header+1, mNone)) return 1;
+
+            int i;
+            for (i = header + 1; i < mRaw.size() && lookingAt(i, p); i++) {
+                s.add(mRaw.get(i).trim());
+            }
+            return i - header;
+        }
+
+        /** Return true if the n'th raw line matches the pattern. */
+        boolean lookingAt(int n, Pattern p) {
+            return p.matcher(mRaw.get(n)).lookingAt();
+        }
+
+        /** Compile the regular expressions the first time. */
+        private static void initialize() {
+            synchronized (sLock) {
+                if (mPool != null) return;
+                mPool = Pattern.compile("Connection pool for (\\S+):");
+                mConnection = Pattern.compile("\\s+Connection #(\\d+):");
+                mEntry = Pattern.compile("\\s+(\\d+): ");
+                mSingleWord = Pattern.compile("  (\\S+)$");
+                mNone = Pattern.compile("\\s+<none>$");
+            }
+        }
+    }
+
+    @Test
+    public void testDumpsys() throws Exception {
+        Dumpsys dumpsys = new Dumpsys();
+
+        assertEquals(1, dumpsys.mDatabase.size());
+        // Note: cannot test mConcurrent because that attribute is not hermitic with respect to
+        // the tests.
+
+        Dumpsys.Database db = dumpsys.mDatabase.get(0);
+
+        // Work with normalized paths.
+        String wantPath = mDatabaseFile.toPath().toRealPath().toString();
+        String realPath = new File(db.mPath).toPath().toRealPath().toString();
+        assertEquals(wantPath, realPath);
+
+        assertEquals(1, db.mConnection.size());
+    }
 }
diff --git a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
index 5464ea3..d6c0e99 100644
--- a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
+++ b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
@@ -16,7 +16,6 @@
 
 package android.hardware.biometrics;
 
-import static android.hardware.biometrics.BiometricPrompt.MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER;
 import static android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.MAX_DESCRIPTION_CHARACTER_NUMBER;
 import static android.hardware.biometrics.PromptVerticalListContentView.MAX_EACH_ITEM_CHARACTER_NUMBER;
 import static android.hardware.biometrics.PromptVerticalListContentView.MAX_ITEM_NUMBER;
@@ -117,19 +116,7 @@
                 () -> new BiometricPrompt.Builder(mContext).setLogoDescription(null)
         );
 
-        assertThat(e).hasMessageThat().contains(
-                "Logo description passed in can not be null or exceed");
-    }
-
-    @Test
-    public void testLogoDescription_charLimit() {
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> new BiometricPrompt.Builder(mContext).setLogoDescription(
-                        generateRandomString(MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + 1))
-        );
-
-        assertThat(e).hasMessageThat().contains(
-                "Logo description passed in can not be null or exceed");
+        assertThat(e).hasMessageThat().isEqualTo("Logo description passed in can not be null");
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java
index 66b22a8..774878a 100644
--- a/core/tests/coretests/src/android/os/FileUtilsTest.java
+++ b/core/tests/coretests/src/android/os/FileUtilsTest.java
@@ -54,7 +54,7 @@
 import static org.junit.Assert.fail;
 
 import android.os.FileUtils.MemoryPipe;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.provider.DocumentsContract.Document;
 import android.system.Os;
@@ -156,7 +156,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_FileToPipe() throws Exception {
         for (int size : DATA_SIZES) {
             final File src = new File(mTarget, "src");
@@ -177,7 +177,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_PipeToFile() throws Exception {
         for (int size : DATA_SIZES) {
             final File dest = new File(mTarget, "dest");
@@ -197,7 +197,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_PipeToPipe() throws Exception {
         for (int size : DATA_SIZES) {
             byte[] expected = new byte[size];
@@ -215,7 +215,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_ShortPipeToFile() throws Exception {
         byte[] source = new byte[33_000_000];
         new Random().nextBytes(source);
@@ -257,9 +257,9 @@
         assertArrayEquals(expected, actual);
     }
 
-    //TODO(ravenwood) Remove the _$noRavenwood suffix and add @RavenwoodIgnore instead
     @Test
-    public void testCopy_SocketToFile_FileToSocket$noRavenwood() throws Exception {
+    @DisabledOnRavenwood(reason = "Missing Os methods in Ravenwood")
+    public void testCopy_SocketToFile_FileToSocket() throws Exception {
         for (int size : DATA_SIZES ) {
             final File src = new File(mTarget, "src");
             final File dest = new File(mTarget, "dest");
@@ -510,7 +510,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_normal() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test"));
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
@@ -530,7 +530,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_unknown() throws Exception {
         assertNameEquals("test",
                 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", "test"));
@@ -544,7 +544,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_dir() throws Exception {
         assertNameEquals("test", FileUtils.buildUniqueFile(mTarget, Document.MIME_TYPE_DIR, "test"));
         new File(mTarget, "test").mkdir();
@@ -559,7 +559,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_increment() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -579,7 +579,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_mimeless() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -675,8 +675,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode() throws Exception {
+    public void testTranslateMode() {
         assertTranslate("r", O_RDONLY, MODE_READ_ONLY);
 
         assertTranslate("rw", O_RDWR | O_CREAT,
@@ -695,8 +694,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testMalformedTransate_int() throws Exception {
+    public void testMalformedTransate_int() {
         try {
             // The non-standard Linux access mode 3 should throw
             // an IllegalArgumentException.
@@ -707,8 +705,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testMalformedTransate_string() throws Exception {
+    public void testMalformedTransate_string() {
         try {
             // The non-standard Linux access mode 3 should throw
             // an IllegalArgumentException.
@@ -719,8 +716,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode_Invalid() throws Exception {
+    public void testTranslateMode_Invalid() {
         try {
             translateModeStringToPosix("rwx");
             fail();
@@ -734,8 +730,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode_Access() throws Exception {
+    public void testTranslateMode_Access() {
         assertEquals(O_RDONLY, translateModeAccessToPosix(F_OK));
         assertEquals(O_RDONLY, translateModeAccessToPosix(R_OK));
         assertEquals(O_WRONLY, translateModeAccessToPosix(W_OK));
@@ -744,7 +739,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
+    @DisabledOnRavenwood(reason = "Requires kernel support")
     public void testConvertToModernFd() throws Exception {
         final String nonce = String.valueOf(System.nanoTime());
 
diff --git a/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java b/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java
new file mode 100644
index 0000000..786c2fc
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.internal.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+/**
+ * Test class for {@link DebugStore}.
+ *
+ * To run it:
+ * atest FrameworksCoreTests:com.android.internal.os.DebugStoreTest
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(blockedBy = DebugStore.class)
+@SmallTest
+public class DebugStoreTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Mock
+    private DebugStore.DebugStoreNative mDebugStoreNativeMock;
+
+    @Captor
+    private ArgumentCaptor<List<String>> mListCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        DebugStore.setDebugStoreNative(mDebugStoreNativeMock);
+    }
+
+    @Test
+    public void testRecordServiceOnStart() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidService"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(1L);
+
+        long eventId = DebugStore.recordServiceOnStart(1, 0, intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcStart"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "stId", "1",
+                "flg", "0",
+                "act", "com.android.ACTION",
+                "comp", "ComponentInfo{com.android/androidService}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(1L);
+    }
+
+    @Test
+    public void testRecordServiceCreate() {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.name = "androidService";
+        serviceInfo.packageName = "com.android";
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(2L);
+
+        long eventId = DebugStore.recordServiceCreate(serviceInfo);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcCreate"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "name", "androidService",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(2L);
+    }
+
+    @Test
+    public void testRecordServiceBind() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidService"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(3L);
+
+        long eventId = DebugStore.recordServiceBind(true, intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcBind"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "rebind", "true",
+                "act", "com.android.ACTION",
+                "cmp", "ComponentInfo{com.android/androidService}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(3L);
+    }
+
+    @Test
+    public void testRecordGoAsync() {
+        DebugStore.recordGoAsync("androidReceiver");
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("GoAsync"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "androidReceiver"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordFinish() {
+        DebugStore.recordFinish("androidReceiver");
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("Finish"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "androidReceiver"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordLongLooperMessage() {
+        DebugStore.recordLongLooperMessage(100, "androidHandler", 500L);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("LooperMsg"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "code", "100",
+                "trgt", "androidHandler",
+                "elapsed", "500"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordBroadcastHandleReceiver() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidReceiver"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(4L);
+
+        long eventId = DebugStore.recordBroadcastHandleReceiver(intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("HandleReceiver"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "act", "com.android.ACTION",
+                "cmp", "ComponentInfo{com.android/androidReceiver}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(4L);
+    }
+
+    @Test
+    public void testRecordEventEnd() {
+        DebugStore.recordEventEnd(1L);
+
+        verify(mDebugStoreNativeMock).endEvent(eq(1L), anyList());
+    }
+
+    @Test
+    public void testRecordServiceOnStartWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(5L);
+
+        long eventId = DebugStore.recordServiceOnStart(1, 0, null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcStart"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "stId", "1",
+                "flg", "0",
+                "act", "null",
+                "comp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(5L);
+    }
+
+    @Test
+    public void testRecordServiceCreateWithNullServiceInfo() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(6L);
+
+        long eventId = DebugStore.recordServiceCreate(null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcCreate"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "name", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(6L);
+    }
+
+    @Test
+    public void testRecordServiceBindWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(7L);
+
+        long eventId = DebugStore.recordServiceBind(false, null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcBind"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "rebind", "false",
+                "act", "null",
+                "cmp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(7L);
+    }
+
+    @Test
+    public void testRecordBroadcastHandleReceiverWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(8L);
+
+        long eventId = DebugStore.recordBroadcastHandleReceiver(null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("HandleReceiver"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "act", "null",
+                "cmp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(8L);
+    }
+
+    @Test
+    public void testRecordGoAsyncWithNullReceiverClassName() {
+        DebugStore.recordGoAsync(null);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("GoAsync"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "null"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordFinishWithNullReceiverClassName() {
+        DebugStore.recordFinish(null);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("Finish"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "null"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordLongLooperMessageWithNullTargetClass() {
+        DebugStore.recordLongLooperMessage(200, null, 1000L);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("LooperMsg"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "code", "200",
+                "trgt", "null",
+                "elapsed", "1000"
+        ).inOrder();
+    }
+}
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
index e8bb710..dd86094 100644
--- a/core/tests/resourceflaggingtests/Android.bp
+++ b/core/tests/resourceflaggingtests/Android.bp
@@ -34,12 +34,24 @@
 }
 
 genrule {
+    name: "resource-flagging-test-app-resources-compile2",
+    tools: ["aapt2"],
+    srcs: [
+        "flagged_resources_res/values/bools2.xml",
+    ],
+    out: ["values_bools2.arsc.flat"],
+    cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
+        "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+}
+
+genrule {
     name: "resource-flagging-test-app-apk",
     tools: ["aapt2"],
     // The first input file in the list must be the manifest
     srcs: [
         "TestAppAndroidManifest.xml",
         ":resource-flagging-test-app-resources-compile",
+        ":resource-flagging-test-app-resources-compile2",
     ],
     out: ["resapp.apk"],
     cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
index f4defd9..8d01465 100644
--- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
+++ b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
@@ -5,4 +5,6 @@
 
     <bool name="res2">false</bool>
     <bool name="res2" android:featureFlag="test.package.trueFlag">true</bool>
+
+    <bool name="res3">false</bool>
 </resources>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml
new file mode 100644
index 0000000..e7563aa
--- /dev/null
+++ b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <bool name="res3" android:featureFlag="test.package.trueFlag">true</bool>
+</resources>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index a0cbe3c..ad8542e 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -63,6 +63,11 @@
         assertThat(getBoolean("res2")).isTrue();
     }
 
+    @Test
+    public void testFlagEnabledDifferentCompilationUnit() {
+        assertThat(getBoolean("res3")).isTrue();
+    }
+
     private boolean getBoolean(String name) {
         int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources");
         assertThat(resId).isNotEqualTo(0);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index b2bc3de..37f0067 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -18,8 +18,8 @@
 
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
 
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
-import static androidx.window.common.CommonFoldingFeature.parseListFromString;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
+import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -31,6 +31,9 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
+import androidx.window.common.layout.CommonFoldingFeature;
+import androidx.window.common.layout.DisplayFoldFeatureCommon;
+
 import com.android.internal.R;
 
 import java.util.ArrayList;
@@ -200,6 +203,23 @@
 
 
     /**
+     * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
+     * {@link DeviceStateManagerFoldingFeatureProducer}.
+     */
+    @NonNull
+    public List<DisplayFoldFeatureCommon> getDisplayFeatures() {
+        final List<DisplayFoldFeatureCommon> foldFeatures = new ArrayList<>();
+        final List<CommonFoldingFeature> folds = getFoldsWithUnknownState();
+
+        final boolean isHalfOpenedSupported = isHalfOpenedSupported();
+        for (CommonFoldingFeature fold : folds) {
+            foldFeatures.add(DisplayFoldFeatureCommon.create(fold, isHalfOpenedSupported));
+        }
+        return foldFeatures;
+    }
+
+
+    /**
      * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
      */
     public boolean isHalfOpenedSupported() {
@@ -211,7 +231,7 @@
      * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
      */
     @Override
-    public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
+    public void getData(@NonNull Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
         mRawFoldSupplier.getData((String displayFeaturesString) -> {
             if (TextUtils.isEmpty(displayFeaturesString)) {
                 storeFeaturesConsumer.accept(new ArrayList<>());
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 6d758f1..9651918 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -26,6 +26,8 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 
+import androidx.window.common.layout.CommonFoldingFeature;
+
 import com.android.internal.R;
 
 import java.util.Optional;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java
new file mode 100644
index 0000000..e72459f
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.window.common.collections;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * A class to contain utility methods for {@link List}.
+ */
+public final class ListUtil {
+
+    private ListUtil() {}
+
+    /**
+     * Returns a new {@link List} that is created by applying the {@code transformer} to the
+     * {@code source} list.
+     */
+    public static <T, U> List<U> map(List<T> source, Function<T, U> transformer) {
+        final List<U> target = new ArrayList<>();
+        for (int i = 0; i < source.size(); i++) {
+            target.add(transformer.apply(source.get(i)));
+        }
+        return target;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
similarity index 99%
rename from libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
index b95bca1..85c4fe1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.common;
+package androidx.window.common.layout;
 
 import static androidx.window.common.ExtensionHelper.isZero;
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java
new file mode 100644
index 0000000..594bd9c
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java
@@ -0,0 +1,171 @@
+/*
+ * 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 androidx.window.common.layout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.ArraySet;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class that represents if a fold is part of the device.
+ */
+public final class DisplayFoldFeatureCommon {
+
+    /**
+     * Returns a new instance of {@link DisplayFoldFeatureCommon} based off of
+     * {@link CommonFoldingFeature} and whether or not half opened is supported.
+     */
+    public static DisplayFoldFeatureCommon create(CommonFoldingFeature foldingFeature,
+            boolean isHalfOpenedSupported) {
+        @FoldType
+        final int foldType;
+        if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) {
+            foldType = DISPLAY_FOLD_FEATURE_TYPE_HINGE;
+        } else {
+            foldType = DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN;
+        }
+
+        final Set<Integer> properties = new ArraySet<>();
+
+        if (isHalfOpenedSupported) {
+            properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        }
+        return new DisplayFoldFeatureCommon(foldType, properties);
+    }
+
+    /**
+     * The type of fold is unknown. This is here for compatibility reasons if a new type is added,
+     * and cannot be reported to an incompatible application.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN = 0;
+
+    /**
+     * The type of fold is a physical hinge separating two display panels.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_HINGE = 1;
+
+    /**
+     * The type of fold is a screen that folds from 0-180.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN, DISPLAY_FOLD_FEATURE_TYPE_HINGE,
+            DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN})
+    public @interface FoldType {
+    }
+
+    /**
+     * The fold supports the half opened state.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED = 1;
+
+    @IntDef(value = {DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED})
+    public @interface FoldProperty {
+    }
+
+    @FoldType
+    private final int mType;
+
+    private final Set<Integer> mProperties;
+
+    /**
+     * Creates an instance of [FoldDisplayFeature].
+     *
+     * @param type                  the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or
+     *                              [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]
+     * @hide
+     */
+    public DisplayFoldFeatureCommon(@FoldType int type, @NonNull Set<Integer> properties) {
+        mType = type;
+        mProperties = new ArraySet<>();
+        assertPropertiesAreValid(properties);
+        mProperties.addAll(properties);
+    }
+
+    /**
+     * Returns the type of fold that is either a hinge or a fold.
+     */
+    @FoldType
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns {@code true} if the fold has the given property, {@code false} otherwise.
+     */
+    public boolean hasProperty(@FoldProperty int property) {
+        return mProperties.contains(property);
+    }
+    /**
+     * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.
+     */
+    public boolean hasProperties(@NonNull @FoldProperty int... properties) {
+        for (int i = 0; i < properties.length; i++) {
+            if (!mProperties.contains(properties[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns a copy of the set of properties.
+     * @hide
+     */
+    public Set<Integer> getProperties() {
+        return new ArraySet<>(mProperties);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DisplayFoldFeatureCommon that = (DisplayFoldFeatureCommon) o;
+        return mType == that.mType && Objects.equals(mProperties, that.mProperties);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mProperties);
+    }
+
+    @Override
+    public String toString() {
+        return "DisplayFoldFeatureCommon{mType=" + mType + ", mProperties=" + mProperties + '}';
+    }
+
+    private static void assertPropertiesAreValid(@NonNull Set<Integer> properties) {
+        for (int property : properties) {
+            if (!isProperty(property)) {
+                throw new IllegalArgumentException("Property is not a valid type: " + property);
+            }
+        }
+    }
+
+    private static boolean isProperty(int property) {
+        if (property == DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 409cde3..7be14724 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -89,9 +89,9 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.extensions.WindowExtensions;
 import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.extensions.core.util.function.Function;
@@ -1334,13 +1334,24 @@
         if (shouldContainerBeExpanded(container)) {
             // Make sure that the existing container is expanded.
             mPresenter.expandTaskFragment(wct, container);
-        } else {
-            // Put activity into a new expanded container.
-            final TaskFragmentContainer newContainer =
-                    new TaskFragmentContainer.Builder(this, getTaskId(activity), activity)
-                            .setPendingAppearedActivity(activity).build();
-            mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
+            return;
         }
+
+        final SplitContainer splitContainer = getActiveSplitForContainer(container);
+        if (splitContainer instanceof SplitPinContainer
+                && !container.isPinned() && container.getRunningActivityCount() == 1) {
+            // This is already the expected state when the pinned container is shown with an
+            // expanded activity in a standalone container on the side. Moving the activity into
+            // another new expanded container again is not necessary and could result in
+            // recursively creating new TaskFragmentContainers if the activity somehow relaunched.
+            return;
+        }
+
+        // Put activity into a new expanded container.
+        final TaskFragmentContainer newContainer =
+                new TaskFragmentContainer.Builder(this, getTaskId(activity), activity)
+                        .setPendingAppearedActivity(activity).build();
+        mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
index a0f481a..870c92e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
@@ -16,48 +16,24 @@
 
 package androidx.window.extensions.layout;
 
-import androidx.window.common.CommonFoldingFeature;
-import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
-
-import java.util.ArrayList;
-import java.util.List;
+import androidx.window.common.layout.DisplayFoldFeatureCommon;
 
 /**
  * Util functions for working with {@link androidx.window.extensions.layout.DisplayFoldFeature}.
  */
-public class DisplayFoldFeatureUtil {
+public final class DisplayFoldFeatureUtil {
 
     private DisplayFoldFeatureUtil() {}
 
-    private static DisplayFoldFeature create(CommonFoldingFeature foldingFeature,
-            boolean isHalfOpenedSupported) {
-        final int foldType;
-        if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) {
-            foldType = DisplayFoldFeature.TYPE_HINGE;
-        } else {
-            foldType = DisplayFoldFeature.TYPE_SCREEN_FOLD_IN;
-        }
-        DisplayFoldFeature.Builder featureBuilder = new DisplayFoldFeature.Builder(foldType);
-
-        if (isHalfOpenedSupported) {
-            featureBuilder.addProperty(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
-        }
-        return featureBuilder.build();
-    }
-
     /**
-     * Returns the list of supported {@link DisplayFeature} calculated from the
-     * {@link DeviceStateManagerFoldingFeatureProducer}.
+     * Returns a {@link DisplayFoldFeature} that matches the given {@link DisplayFoldFeatureCommon}.
      */
-    public static List<DisplayFoldFeature> extractDisplayFoldFeatures(
-            DeviceStateManagerFoldingFeatureProducer producer) {
-        List<DisplayFoldFeature> foldFeatures = new ArrayList<>();
-        List<CommonFoldingFeature> folds = producer.getFoldsWithUnknownState();
-
-        final boolean isHalfOpenedSupported = producer.isHalfOpenedSupported();
-        for (CommonFoldingFeature fold : folds) {
-            foldFeatures.add(DisplayFoldFeatureUtil.create(fold, isHalfOpenedSupported));
+    public static DisplayFoldFeature translate(DisplayFoldFeatureCommon foldFeatureCommon) {
+        final DisplayFoldFeature.Builder builder =
+                new DisplayFoldFeature.Builder(foldFeatureCommon.getType());
+        for (int property: foldFeatureCommon.getProperties()) {
+            builder.addProperty(property);
         }
-        return foldFeatures;
+        return builder.build();
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index a3ef68a..f1ea19a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -19,11 +19,11 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
 import static androidx.window.common.ExtensionHelper.isZero;
 import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
 import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
 
 import android.app.Activity;
 import android.app.ActivityThread;
@@ -45,9 +45,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiContext;
 import androidx.annotation.VisibleForTesting;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.common.collections.ListUtil;
+import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.extensions.util.DeduplicateConsumer;
 
@@ -95,8 +96,8 @@
                 .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
         mFoldingFeatureProducer = foldingFeatureProducer;
         mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
-        final List<DisplayFoldFeature> displayFoldFeatures =
-                DisplayFoldFeatureUtil.extractDisplayFoldFeatures(mFoldingFeatureProducer);
+        final List<DisplayFoldFeature> displayFoldFeatures = ListUtil.map(
+                mFoldingFeatureProducer.getDisplayFeatures(), DisplayFoldFeatureUtil::translate);
         mSupportedWindowFeatures = new SupportedWindowFeatures.Builder(displayFoldFeatures).build();
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index b63fd08..60bc7be 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -26,10 +26,10 @@
 
 import androidx.annotation.NonNull;
 import androidx.window.common.BaseDataProducer;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
 import androidx.window.common.RawFoldingFeatureProducer;
+import androidx.window.common.layout.CommonFoldingFeature;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
index 4fd03e4..6e0e711 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -26,7 +26,7 @@
 import android.graphics.Rect;
 import android.os.IBinder;
 
-import androidx.window.common.CommonFoldingFeature;
+import androidx.window.common.layout.CommonFoldingFeature;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java
new file mode 100644
index 0000000..a077bdf
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 androidx.window.common.collections;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test class for {@link ListUtil}.
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:ListUtil
+ */
+public class ListUtilTest {
+
+    @Test
+    public void test_map_empty_returns_empty() {
+        final List<String> emptyList = new ArrayList<>();
+        final List<Integer> result = ListUtil.map(emptyList, String::length);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void test_map_maintains_order() {
+        final List<String> source = new ArrayList<>();
+        source.add("a");
+        source.add("aa");
+
+        final List<Integer> result = ListUtil.map(source, String::length);
+
+        assertThat(result).containsExactly(1, 2).inOrder();
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java
new file mode 100644
index 0000000..6c17851
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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 androidx.window.common.layout;
+
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_FOLD;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_HINGE;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_HINGE;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+/**
+ * Test class for {@link DisplayFoldFeatureCommon}.
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:DisplayFoldFeatureCommonTest
+ */
+public class DisplayFoldFeatureCommonTest {
+
+    @Test
+    public void test_different_type_not_equals() {
+        final Set<Integer> properties = new ArraySet<>();
+        final DisplayFoldFeatureCommon first =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+        final DisplayFoldFeatureCommon second =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, properties);
+
+        assertThat(first).isEqualTo(second);
+    }
+
+    @Test
+    public void test_different_property_set_not_equals() {
+        final Set<Integer> firstProperties = new ArraySet<>();
+        final Set<Integer> secondProperties = new ArraySet<>();
+        secondProperties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon first =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, firstProperties);
+        final DisplayFoldFeatureCommon second =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, secondProperties);
+
+        assertThat(first).isNotEqualTo(second);
+    }
+
+    @Test
+    public void test_check_single_property_exists() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_check_multiple_properties_exists() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.hasProperties(
+                DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_properties_matches_getter() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.getProperties()).isEqualTo(properties);
+    }
+
+    @Test
+    public void test_type_matches_getter() {
+        final Set<Integer> properties = new ArraySet<>();
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE);
+    }
+
+    @Test
+    public void test_create_half_opened_feature() {
+        final CommonFoldingFeature foldingFeature =
+                new CommonFoldingFeature(COMMON_TYPE_HINGE, COMMON_STATE_UNKNOWN, new Rect());
+        final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create(
+                foldingFeature, true);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE);
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_create_fold_feature_no_half_opened() {
+        final CommonFoldingFeature foldingFeature =
+                new CommonFoldingFeature(COMMON_TYPE_FOLD, COMMON_STATE_UNKNOWN, new Rect());
+        final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create(
+                foldingFeature, true);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN);
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+}
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml
new file mode 100644
index 0000000..c154059
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+  <path
+      android:pathData="M15 16V14H13V12.5H15V10.5H16.5V12.5H18.5V14H16.5V16H15ZM3.5 17C3.09722 17 2.74306 16.8542 2.4375 16.5625C2.14583 16.2569 2 15.9028 2 15.5V4.5C2 4.08333 2.14583 3.72917 2.4375 3.4375C2.74306 3.14583 3.09722 3 3.5 3H14.5C14.9167 3 15.2708 3.14583 15.5625 3.4375C15.8542 3.72917 16 4.08333 16 4.5V9H14.5V7H3.5V15.5H13.625V17H3.5ZM3.5 5.5H14.5V4.5H3.5V5.5ZM3.5 5.5V4.5V5.5Z"
+      android:fillColor="#1C1C14"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 49d9029..eea3de8 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -125,7 +125,7 @@
     <LinearLayout
         android:id="@+id/more_actions_pill"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height"
+        android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
         android:layout_marginStart="1dp"
         android:orientation="vertical"
@@ -139,6 +139,14 @@
             android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
             android:drawableTint="?androidprv:attr/materialColorOnSurface"
             style="@style/DesktopModeHandleMenuActionButton"/>
+
+        <Button
+            android:id="@+id/new_window_button"
+            android:contentDescription="@string/new_window_text"
+            android:text="@string/new_window_text"
+            android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window"
+            android:drawableTint="?androidprv:attr/materialColorOnSurface"
+            style="@style/DesktopModeHandleMenuActionButton" />
     </LinearLayout>
 
     <LinearLayout
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 1eb2458..e6807ac 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -497,7 +497,7 @@
 
     <!-- The maximum height of the handle menu in desktop mode. Four pills (52dp each) plus 2dp
         spacing between them plus 4dp top padding. -->
-    <dimen name="desktop_mode_handle_menu_height">218dp</dimen>
+    <dimen name="desktop_mode_handle_menu_height">270dp</dimen>
 
     <!-- The elevation set on the handle menu pills. -->
     <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen>
@@ -508,8 +508,11 @@
     <!-- The height of the handle menu's "Windowing" pill in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_windowing_pill_height">52dp</dimen>
 
-    <!-- The height of the handle menu's "More Actions" pill in desktop mode. -->
-    <dimen name="desktop_mode_handle_menu_more_actions_pill_height">52dp</dimen>
+    <!-- The maximum height of the handle menu's "New Window" button in desktop mode. -->
+    <dimen name="desktop_mode_handle_menu_new_window_height">52dp</dimen>
+
+    <!-- The maximum height of the handle menu's "Screenshot" button in desktop mode. -->
+    <dimen name="desktop_mode_handle_menu_screenshot_height">52dp</dimen>
 
     <!-- The height of the handle menu's "Open in browser" pill in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 8669af3..0a8166f 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -282,6 +282,8 @@
     <string name="screenshot_text">Screenshot</string>
     <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] -->
     <string name="open_in_browser_text">Open in browser</string>
+    <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] -->
+    <string name="new_window_text">New Window</string>
     <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] -->
     <string name="close_text">Close</string>
     <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 2493e98..6d63971 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -46,7 +46,8 @@
     DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
     ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
     BACK_NAVIGATION(Flags::enableDesktopWindowingBackNavigation, true),
-    EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true);
+    EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
+    TASKBAR_RUNNING_APPS(Flags::enableDesktopWindowingTaskbarRunningApps, true);
 
     /**
      * Determines state of flag based on the actual flag and desktop mode developer option
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index a9fdea3..f14f419 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -16,7 +16,12 @@
 
 package com.android.wm.shell.back;
 
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
+import static com.android.window.flags.Flags.migratePredictiveBackTransition;
 import static com.android.window.flags.Flags.predictiveBackSystemAnims;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
@@ -30,11 +35,13 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -48,6 +55,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.BackAnimationAdapter;
 import android.window.BackEvent;
@@ -57,6 +65,9 @@
 import android.window.IBackAnimationFinishedCallback;
 import android.window.IBackAnimationRunner;
 import android.window.IOnBackInvokedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLog;
@@ -66,12 +77,14 @@
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -101,6 +114,7 @@
 
     /** Tracks if an uninterruptible animation is in progress */
     private boolean mPostCommitAnimationInProgress = false;
+    private boolean mRealCallbackInvoked = false;
 
     /** Tracks if we should start the back gesture on the next motion move event */
     private boolean mShouldStartOnNextMoveEvent = false;
@@ -123,6 +137,8 @@
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
     private final WindowManager mWindowManager;
+    private final Transitions mTransitions;
+    private final BackTransitionHandler mBackTransitionHandler;
     @VisibleForTesting
     final Rect mTouchableArea = new Rect();
 
@@ -190,7 +206,8 @@
             Context context,
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
-            ShellCommandHandler shellCommandHandler) {
+            ShellCommandHandler shellCommandHandler,
+            Transitions transitions) {
         this(
                 shellInit,
                 shellController,
@@ -201,7 +218,8 @@
                 context.getContentResolver(),
                 backAnimationBackground,
                 shellBackAnimationRegistry,
-                shellCommandHandler);
+                shellCommandHandler,
+                transitions);
     }
 
     @VisibleForTesting
@@ -215,7 +233,8 @@
             ContentResolver contentResolver,
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
-            ShellCommandHandler shellCommandHandler) {
+            ShellCommandHandler shellCommandHandler,
+            Transitions transitions) {
         mShellController = shellController;
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
@@ -230,6 +249,9 @@
         mLatencyTracker = LatencyTracker.getInstance(mContext);
         mShellCommandHandler = shellCommandHandler;
         mWindowManager = context.getSystemService(WindowManager.class);
+        mTransitions = transitions;
+        mBackTransitionHandler = new BackTransitionHandler();
+        mTransitions.addHandler(mBackTransitionHandler);
         updateTouchableArea();
     }
 
@@ -730,7 +752,7 @@
             mBackAnimationFinishedCallback = null;
         }
 
-        if (mBackNavigationInfo != null) {
+        if (mBackNavigationInfo != null && !mRealCallbackInvoked) {
             final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
             if (touchTracker.getTriggerBack()) {
                 dispatchOnBackInvoked(callback);
@@ -738,6 +760,7 @@
                 tryDispatchOnBackCancelled(callback);
             }
         }
+        mRealCallbackInvoked = false;
         finishBackNavigation(touchTracker.getTriggerBack());
     }
 
@@ -815,14 +838,38 @@
 
         // The next callback should be {@link #onBackAnimationFinished}.
         if (mCurrentTracker.getTriggerBack()) {
-            // notify gesture finished
-            mBackNavigationInfo.onBackGestureFinished(true);
+            if (migratePredictiveBackTransition()) {
+                // notify core gesture is commit
+                if (shouldTriggerCloseTransition()) {
+                    mBackTransitionHandler.mCloseTransitionRequested = true;
+                    final IOnBackInvokedCallback callback =
+                            mBackNavigationInfo.getOnBackInvokedCallback();
+                    // invoked client side onBackInvoked
+                    dispatchOnBackInvoked(callback);
+                    mRealCallbackInvoked = true;
+                }
+            } else {
+                // notify gesture finished
+                mBackNavigationInfo.onBackGestureFinished(true);
+            }
+
+            // start post animation
             dispatchOnBackInvoked(mActiveCallback);
         } else {
             tryDispatchOnBackCancelled(mActiveCallback);
         }
     }
 
+    // Close window won't create any transition
+    private boolean shouldTriggerCloseTransition() {
+        if (mBackNavigationInfo == null) {
+            return false;
+        }
+        int type = mBackNavigationInfo.getType();
+        return type == BackNavigationInfo.TYPE_RETURN_TO_HOME
+                || type == BackNavigationInfo.TYPE_CROSS_TASK
+                || type == BackNavigationInfo.TYPE_CROSS_ACTIVITY;
+    }
     /**
      * Called when the post commit animation is completed or timeout.
      * This will trigger the real {@link IOnBackInvokedCallback} behavior.
@@ -857,6 +904,7 @@
                     "mCurrentBackGestureInfo was null when back animation finished");
         }
         resetTouchTracker();
+        mBackTransitionHandler.onAnimationFinished();
     }
 
     /**
@@ -1016,11 +1064,13 @@
                                     endLatencyTracking();
                                     if (!validateAnimationTargets(apps)) {
                                         Log.e(TAG, "Invalid animation targets!");
+                                        mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
                                         return;
                                     }
                                     mBackAnimationFinishedCallback = finishedCallback;
                                     mApps = apps;
                                     startSystemAnimation();
+                                    mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
 
                                     // Dispatch the first progress after animation start for
                                     // smoothing the initial animation, instead of waiting for next
@@ -1041,6 +1091,7 @@
                     public void onAnimationCancelled() {
                         mShellExecutor.execute(
                                 () -> {
+                                    mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
                                     if (!mShellBackAnimationRegistry.cancel(
                                             mBackNavigationInfo != null
                                                     ? mBackNavigationInfo.getType()
@@ -1073,4 +1124,249 @@
         mQueuedTracker.dump(pw, prefix + "    ");
     }
 
+    class BackTransitionHandler implements Transitions.TransitionHandler {
+
+        Runnable mOnAnimationFinishCallback;
+        boolean mCloseTransitionRequested;
+        boolean mOpeningRunning;
+        SurfaceControl.Transaction mFinishOpenTransaction;
+        Transitions.TransitionFinishCallback mFinishOpenTransitionCallback;
+        QueuedTransition mQueuedTransition = null;
+        void onAnimationFinished() {
+            if (!mCloseTransitionRequested) {
+                applyFinishOpenTransition();
+            }
+            if (mOnAnimationFinishCallback != null) {
+                mOnAnimationFinishCallback.run();
+                mOnAnimationFinishCallback = null;
+            }
+        }
+
+        void consumeQueuedTransitionIfNeeded() {
+            if (mQueuedTransition != null) {
+                mQueuedTransition.consume();
+                mQueuedTransition = null;
+            }
+        }
+
+        private void applyFinishOpenTransition() {
+            if (mFinishOpenTransaction != null) {
+                mFinishOpenTransaction.apply();
+                mFinishOpenTransaction = null;
+            }
+            if (mFinishOpenTransitionCallback != null) {
+                mFinishOpenTransitionCallback.onTransitionFinished(null);
+                mFinishOpenTransitionCallback = null;
+            }
+            mOpeningRunning = false;
+        }
+
+        private void applyAndFinish(@NonNull SurfaceControl.Transaction st,
+                @NonNull SurfaceControl.Transaction ft,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            applyFinishOpenTransition();
+            st.apply();
+            ft.apply();
+            finishCallback.onTransitionFinished(null);
+            mCloseTransitionRequested = false;
+        }
+        @Override
+        public boolean startAnimation(@NonNull IBinder transition,
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction st,
+                @NonNull SurfaceControl.Transaction ft,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't
+            // need to post to ShellExecutor when called.
+            if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION
+                    && !isGestureBackTransition(info)) {
+                return false;
+            }
+            if (mApps == null || mApps.length == 0) {
+                if (mBackNavigationInfo != null && mShellBackAnimationRegistry
+                        .isWaitingAnimation(mBackNavigationInfo.getType())) {
+                    // Waiting for animation? Queue update to wait for animation start.
+                    consumeQueuedTransitionIfNeeded();
+                    mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback);
+                } else {
+                    // animation was done, consume directly
+                    applyAndFinish(st, ft, finishCallback);
+                }
+                return true;
+            }
+
+            if (handlePrepareTransition(info, st, ft, finishCallback)) {
+                return true;
+            }
+            return handleCloseTransition(info, st, ft, finishCallback);
+        }
+
+        @Override
+        public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            if (!isGestureBackTransition(info)) {
+                if (mOpeningRunning) {
+                    applyFinishOpenTransition();
+                }
+                if (mQueuedTransition != null) {
+                    consumeQueuedTransitionIfNeeded();
+                }
+                return;
+            }
+            // Handle the commit transition if this handler is running the open transition.
+            finishCallback.onTransitionFinished(null);
+            if (mCloseTransitionRequested) {
+                if (mApps == null || mApps.length == 0) {
+                    if (mQueuedTransition == null) {
+                        // animation was done
+                        applyFinishOpenTransition();
+                        mCloseTransitionRequested = false;
+                    } // else, let queued transition to play
+                } else {
+                    // we are animating, wait until animation finish
+                    mOnAnimationFinishCallback = () -> {
+                        applyFinishOpenTransition();
+                        mCloseTransitionRequested = false;
+                    };
+                }
+            }
+        }
+
+        /**
+         * Check whether this transition is prepare for predictive back animation, which could
+         * happen when core make an activity become visible.
+         */
+        private boolean handlePrepareTransition(
+                @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction st,
+                @NonNull SurfaceControl.Transaction ft,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
+                return false;
+            }
+
+            SurfaceControl openingLeash = null;
+            for (int i = mApps.length - 1; i >= 0; --i) {
+                if (mApps[i].mode == MODE_OPENING) {
+                    openingLeash = mApps[i].leash;
+                }
+            }
+            if (openingLeash != null) {
+                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                    final TransitionInfo.Change c = info.getChanges().get(i);
+                    if (TransitionUtil.isOpeningMode(c.getMode())) {
+                        final Point offset = c.getEndRelOffset();
+                        st.setPosition(c.getLeash(), offset.x, offset.y);
+                        st.reparent(c.getLeash(), openingLeash);
+                    }
+                }
+            }
+            st.apply();
+            mFinishOpenTransaction = ft;
+            mFinishOpenTransitionCallback = finishCallback;
+            mOpeningRunning = true;
+            return true;
+        }
+
+        private boolean isGestureBackTransition(@NonNull TransitionInfo info) {
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                final TransitionInfo.Change c = info.getChanges().get(i);
+                if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
+                        && (TransitionUtil.isOpeningMode(c.getMode())
+                        || TransitionUtil.isClosingMode(c.getMode()))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        /**
+         * Check whether this transition is triggered from back gesture commitment.
+         * Reparent the transition targets to animation leashes, so the animation won't be broken.
+         */
+        private boolean handleCloseTransition(@NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction st,
+                @NonNull SurfaceControl.Transaction ft,
+                @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            SurfaceControl openingLeash = null;
+            SurfaceControl closingLeash = null;
+            for (int i = mApps.length - 1; i >= 0; --i) {
+                if (mApps[i].mode == MODE_OPENING) {
+                    openingLeash = mApps[i].leash;
+                }
+                if (mApps[i].mode == MODE_CLOSING) {
+                    closingLeash = mApps[i].leash;
+                }
+            }
+            if (openingLeash != null && closingLeash != null) {
+                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                    final TransitionInfo.Change c = info.getChanges().get(i);
+                    if (TransitionUtil.isOpeningMode(c.getMode())) {
+                        final Point offset = c.getEndRelOffset();
+                        st.setPosition(c.getLeash(), offset.x, offset.y);
+                        st.reparent(c.getLeash(), openingLeash);
+                    } else if (TransitionUtil.isClosingMode(c.getMode())) {
+                        st.reparent(c.getLeash(), closingLeash);
+                    }
+                }
+            }
+            st.apply();
+            // mApps must exists
+            mOnAnimationFinishCallback = () -> {
+                ft.apply();
+                finishCallback.onTransitionFinished(null);
+                mCloseTransitionRequested = false;
+            };
+            return true;
+        }
+
+        @Nullable
+        @Override
+        public WindowContainerTransaction handleRequest(
+                @NonNull IBinder transition,
+                @NonNull TransitionRequestInfo request) {
+            if (request.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
+                return new WindowContainerTransaction();
+            }
+            if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) {
+                return new WindowContainerTransaction();
+            }
+            return null;
+        }
+
+        class QueuedTransition {
+            final TransitionInfo mInfo;
+            final SurfaceControl.Transaction mSt;
+            final  SurfaceControl.Transaction mFt;
+            final Transitions.TransitionFinishCallback mFinishCallback;
+            QueuedTransition(@NonNull TransitionInfo info,
+                    @NonNull SurfaceControl.Transaction st,
+                    @NonNull SurfaceControl.Transaction ft,
+                    @NonNull Transitions.TransitionFinishCallback finishCallback) {
+                mInfo = info;
+                mSt = st;
+                mFt = ft;
+                mFinishCallback = finishCallback;
+            }
+
+            void consume() {
+                // not animating, consume transition directly
+                if (mApps == null || mApps.length == 0) {
+                    applyAndFinish(mSt, mFt, mFinishCallback);
+                    return;
+                }
+                // we are animating
+                if (handlePrepareTransition(mInfo, mSt, mFt, mFinishCallback)) {
+                    // handle merge transition if any
+                    if (mCloseTransitionRequested) {
+                        mOnAnimationFinishCallback = () -> {
+                            applyFinishOpenTransition();
+                            mCloseTransitionRequested = false;
+                        };
+                    }
+                }
+                handleCloseTransition(mInfo, mSt, mFt, mFinishCallback);
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
index a6be640..4cd2fd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
@@ -22,7 +22,6 @@
 import android.content.pm.PackageManager
 import android.os.UserHandle
 import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
-import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.R
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
@@ -41,7 +40,6 @@
     /**
      * Returns whether a specific component desires to be launched in multiple instances.
      */
-    @VisibleForTesting
     fun supportsMultiInstanceSplit(componentName: ComponentName?): Boolean {
         if (componentName == null || componentName.packageName == null) {
             // TODO(b/262864589): Handle empty component case
@@ -60,7 +58,7 @@
 
         if (!supportsMultiInstanceProperty) {
             // If not checking the multi-instance properties, then return early
-            return false;
+            return false
         }
 
         // Check the activity property first
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 bc6ed1f..2e1789a 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
@@ -16,7 +16,6 @@
 
 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;
 
@@ -29,13 +28,8 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition;
 
-import android.content.Context;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.hardware.display.DisplayManager;
-import android.view.Display;
-import android.view.DisplayInfo;
 
 import androidx.annotation.Nullable;
 
@@ -86,7 +80,7 @@
     private final float mFixedRatio;
     /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */
     private final boolean mAllowFlexibleSplitRatios;
-    private boolean mIsHorizontalDivision;
+    private final boolean mIsHorizontalDivision;
 
     /** The first target which is still splitting the screen */
     private final SnapTarget mFirstSplitTarget;
@@ -98,27 +92,6 @@
     private final SnapTarget mDismissEndTarget;
     private final SnapTarget mMiddleTarget;
 
-    public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
-        DisplayInfo displayInfo = new DisplayInfo();
-        ctx.getSystemService(DisplayManager.class).getDisplay(
-                Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
-        int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.docked_stack_divider_thickness);
-        int dividerInsets = ctx.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.docked_stack_divider_insets);
-        return new DividerSnapAlgorithm(ctx.getResources(),
-                displayInfo.logicalWidth, displayInfo.logicalHeight,
-                dividerWindowWidth - 2 * dividerInsets,
-                ctx.getApplicationContext().getResources().getConfiguration().orientation
-                        == Configuration.ORIENTATION_PORTRAIT,
-                insets);
-    }
-
-    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
-            boolean isHorizontalDivision, Rect insets) {
-        this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
-                DOCKED_INVALID, false /* minimized */, true /* resizable */);
-    }
 
     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
         boolean isHorizontalDivision, Rect insets, int dockSide) {
@@ -160,29 +133,11 @@
     }
 
     /**
-     * @return whether it's feasible to enable split screen in the current configuration, i.e. when
-     *         snapping in the middle both tasks are larger than the minimal task size.
-     */
-    public boolean isSplitScreenFeasible() {
-        int statusBarSize = mInsets.top;
-        int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
-        int size = mIsHorizontalDivision
-                ? mDisplayHeight
-                : mDisplayWidth;
-        int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
-        return availableSpace / 2 >= mMinimalSizeResizableTask;
-    }
-
-    public SnapTarget calculateSnapTarget(int position, float velocity) {
-        return calculateSnapTarget(position, velocity, true /* hardDismiss */);
-    }
-
-    /**
      * @param position the top/left position of the divider
      * @param velocity current dragging velocity
-     * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
+     * @param hardToDismiss if set, make it a bit harder to get reach the dismiss targets
      */
-    public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
+    public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardToDismiss) {
         if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
             return mDismissStartTarget;
         }
@@ -190,7 +145,7 @@
             return mDismissEndTarget;
         }
         if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
-            return snap(position, hardDismiss);
+            return snap(position, hardToDismiss);
         }
         if (velocity < 0) {
             return mFirstSplitTarget;
@@ -236,19 +191,6 @@
         return 0f;
     }
 
-    public SnapTarget getClosestDismissTarget(int position) {
-        if (position < mFirstSplitTarget.position) {
-            return mDismissStartTarget;
-        } else if (position > mLastSplitTarget.position) {
-            return mDismissEndTarget;
-        } else if (position - mDismissStartTarget.position
-                < mDismissEndTarget.position - position) {
-            return mDismissStartTarget;
-        } else {
-            return mDismissEndTarget;
-        }
-    }
-
     public SnapTarget getFirstSplitTarget() {
         return mFirstSplitTarget;
     }
@@ -411,22 +353,6 @@
         return mMiddleTarget;
     }
 
-    public SnapTarget getNextTarget(SnapTarget snapTarget) {
-        int index = mTargets.indexOf(snapTarget);
-        if (index != -1 && index < mTargets.size() - 1) {
-            return mTargets.get(index + 1);
-        }
-        return snapTarget;
-    }
-
-    public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
-        int index = mTargets.indexOf(snapTarget);
-        if (index != -1 && index > 0) {
-            return mTargets.get(index - 1);
-        }
-        return snapTarget;
-    }
-
     /**
      * @return whether or not there are more than 1 split targets that do not include the two
      * dismiss targets, used in deciding to display the middle target for accessibility
@@ -451,26 +377,6 @@
     }
 
     /**
-     * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
-     * if {@param increment} is negative and moves right otherwise.
-     */
-    public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
-        int index = mTargets.indexOf(snapTarget);
-        if (index != -1) {
-            SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
-                    % mTargets.size());
-            if (newTarget == mDismissStartTarget) {
-                return mLastSplitTarget;
-            } else if (newTarget == mDismissEndTarget) {
-                return mFirstSplitTarget;
-            } else {
-                return newTarget;
-            }
-        }
-        return snapTarget;
-    }
-
-    /**
      * Represents a snap target for the divider.
      */
     public static class SnapTarget {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 08de900..271c07d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -19,6 +19,8 @@
 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
 import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskInfo;
@@ -38,7 +40,6 @@
 import com.android.wm.shell.compatui.api.CompatUIEvent;
 import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.util.function.Consumer;
 
@@ -82,7 +83,7 @@
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mCallback = callback;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
-        if (DesktopModeStatus.canEnterDesktopMode(context)
+        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
                 && DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
             // Don't show the SCM button for freeform tasks
             mHasSizeCompat &= !taskInfo.isFreeform();
@@ -138,7 +139,7 @@
             boolean canShow) {
         final boolean prevHasSizeCompat = mHasSizeCompat;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
-        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
                 && DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)) {
             // Don't show the SCM button for freeform tasks
             mHasSizeCompat &= !taskInfo.isFreeform();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 2dc6382..717a414 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -375,7 +375,8 @@
             @ShellBackgroundThread Handler backgroundHandler,
             BackAnimationBackground backAnimationBackground,
             Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry,
-            ShellCommandHandler shellCommandHandler) {
+            ShellCommandHandler shellCommandHandler,
+            Transitions transitions) {
         if (BackAnimationController.IS_ENABLED) {
             return shellBackAnimationRegistry.map(
                     (animations) ->
@@ -387,7 +388,8 @@
                                     context,
                                     backAnimationBackground,
                                     animations,
-                                    shellCommandHandler));
+                                    shellCommandHandler,
+                                    transitions));
         }
         return Optional.empty();
     }
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 da1af0d..9cfbde4 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
@@ -226,7 +226,8 @@
             Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             InteractionJankMonitor interactionJankMonitor,
-            AppToWebGenericLinksParser genericLinksParser) {
+            AppToWebGenericLinksParser genericLinksParser,
+            MultiInstanceHelper multiInstanceHelper) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return new DesktopModeWindowDecorViewModel(
                     context,
@@ -246,7 +247,8 @@
                     desktopTasksController,
                     rootTaskDisplayAreaOrganizer,
                     interactionJankMonitor,
-                    genericLinksParser);
+                    genericLinksParser,
+                    multiInstanceHelper);
         }
         return new CaptionWindowDecorViewModel(
                 context,
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 7175ec4..3e417b6 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
@@ -98,6 +98,7 @@
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
 import com.android.wm.shell.windowdecor.extension.isFullscreen
+import com.android.wm.shell.windowdecor.extension.isMultiWindow
 import java.io.PrintWriter
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -638,16 +639,23 @@
     /**
      * Quick-resize to the right or left half of the stable bounds.
      *
+     * @param taskInfo current task that is being snap-resized via dragging or maximize menu button
+     * @param currentDragBounds current position of the task leash being dragged (or current task
+     *                          bounds if being snapped resize via maximize menu button)
      * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to.
      */
-    fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) {
+    fun snapToHalfScreen(
+        taskInfo: RunningTaskInfo,
+        currentDragBounds: Rect,
+        position: SnapPosition
+    ) {
         val destinationBounds = getSnapBounds(taskInfo, position)
 
         if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return
 
         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
+            toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
         }
@@ -818,8 +826,12 @@
         // Check if we should skip handling this transition
         var reason = ""
         val triggerTask = request.triggerTask
+        var shouldHandleMidRecentsFreeformLaunch =
+            recentsAnimationRunning && isFreeformRelaunch(triggerTask, request)
         val shouldHandleRequest =
             when {
+                // Handle freeform relaunch during recents animation
+                shouldHandleMidRecentsFreeformLaunch -> true
                 recentsAnimationRunning -> {
                     reason = "recents animation is running"
                     false
@@ -859,6 +871,8 @@
         val result =
             triggerTask?.let { task ->
                 when {
+                    // Check if freeform task launch during recents should be handled
+                    shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
                     // Check if the closing task needs to be handled
                     TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
                     // Check if the top task shouldn't be allowed to enter desktop mode
@@ -892,6 +906,12 @@
             .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
     }
 
+    /** Returns whether an existing desktop task is being relaunched in freeform or not. */
+    private fun isFreeformRelaunch(triggerTask: RunningTaskInfo?, request: TransitionRequestInfo) =
+        (triggerTask != null && triggerTask.windowingMode == WINDOWING_MODE_FREEFORM
+                && TransitionUtil.isOpeningType(request.type)
+                && taskRepository.isActiveTask(triggerTask.taskId))
+
     private fun isIncompatibleTask(task: TaskInfo) =
         DesktopModeFlags.MODALS_POLICY.isEnabled(context)
                 && isTopActivityExemptFromDesktopWindowing(context, task)
@@ -902,6 +922,75 @@
             request.triggerTask != null
     }
 
+    fun openNewWindow(
+        taskInfo: RunningTaskInfo
+    ) {
+        // TODO(b/337915660): Add a transition handler for these; animations
+        //  need updates in some cases.
+        val newTaskWindowingMode = when {
+            taskInfo.isFreeform -> {
+                WINDOWING_MODE_FREEFORM
+            }
+            taskInfo.isFullscreen || taskInfo.isMultiWindow -> {
+                WINDOWING_MODE_MULTI_WINDOW
+            }
+            else -> {
+                error("Invalid windowing mode: ${taskInfo.windowingMode}")
+            }
+        }
+
+        val baseActivity = taskInfo.baseActivity ?: return
+        val fillIn: Intent = context.packageManager
+            .getLaunchIntentForPackage(
+                baseActivity.packageName
+            ) ?: return
+        fillIn
+            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+        val options =
+            ActivityOptions.makeBasic().apply {
+                launchWindowingMode = newTaskWindowingMode
+                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+                pendingIntentBackgroundActivityStartMode =
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            }
+        val launchIntent = PendingIntent.getActivity(
+            context,
+            /* requestCode= */ 0,
+            fillIn,
+            PendingIntent.FLAG_IMMUTABLE
+        )
+        when (newTaskWindowingMode) {
+            WINDOWING_MODE_MULTI_WINDOW -> {
+                val splitPosition = splitScreenController.determineNewInstancePosition(taskInfo)
+                splitScreenController.startIntent(
+                    launchIntent, context.userId, fillIn, splitPosition,
+                    options.toBundle(), null /* hideTaskToken */
+                )
+            }
+            WINDOWING_MODE_FREEFORM -> {
+                // TODO(b/336289597): This currently does not respect the desktop window limit.
+                val wct = WindowContainerTransaction()
+                wct.sendPendingIntent(launchIntent, fillIn, options.toBundle())
+                transitions.startTransition(TRANSIT_OPEN, wct, null)
+            }
+        }
+    }
+
+    /**
+     * Handles the case where a freeform task is launched from recents.
+     *
+     * This is a special case where we want to launch the task in fullscreen instead of freeform.
+     */
+    private fun handleMidRecentsFreeformTaskLaunch(
+        task: RunningTaskInfo
+    ): WindowContainerTransaction? {
+        logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch")
+        val wct = WindowContainerTransaction()
+        addMoveToFullscreenChanges(wct, task)
+        wct.reorder(task.token, true)
+        return wct
+    }
+
     private fun handleFreeformTaskLaunch(
         task: RunningTaskInfo,
         transition: IBinder
@@ -1184,7 +1273,7 @@
         taskSurface: SurfaceControl,
         inputX: Float,
         taskTop: Float
-    ): DesktopModeVisualIndicator.IndicatorType {
+    ): IndicatorType {
         // If the visual indicator does not exist, create it.
         val indicator =
             visualIndicator
@@ -1207,13 +1296,15 @@
      * @param taskInfo the task being dragged.
      * @param position position of surface when drag ends.
      * @param inputCoordinate the coordinates of the motion event
-     * @param taskBounds the updated bounds of the task being dragged.
+     * @param currentDragBounds the current bounds of where the visible task is (might be actual
+     *                          task bounds or just task leash)
+     * @param validDragArea the bounds of where the task can be dragged within the display.
      */
     fun onDragPositioningEnd(
         taskInfo: RunningTaskInfo,
         position: Point,
         inputCoordinate: PointF,
-        taskBounds: Rect,
+        currentDragBounds: Rect,
         validDragArea: Rect
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
@@ -1223,41 +1314,40 @@
         val indicator = visualIndicator ?: return
         val indicatorType =
             indicator.updateIndicatorType(
-                PointF(inputCoordinate.x, taskBounds.top.toFloat()),
+                PointF(inputCoordinate.x, currentDragBounds.top.toFloat()),
                 taskInfo.windowingMode
             )
         when (indicatorType) {
-            DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+            IndicatorType.TO_FULLSCREEN_INDICATOR -> {
                 moveToFullscreenWithAnimation(
                     taskInfo,
                     position,
                     DesktopModeTransitionSource.TASK_DRAG
                 )
             }
-            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
+            IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                 releaseVisualIndicator()
-                snapToHalfScreen(taskInfo, SnapPosition.LEFT)
+                snapToHalfScreen(taskInfo, currentDragBounds, SnapPosition.LEFT)
             }
-            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+            IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
                 releaseVisualIndicator()
-                snapToHalfScreen(taskInfo, SnapPosition.RIGHT)
+                snapToHalfScreen(taskInfo, currentDragBounds, SnapPosition.RIGHT)
             }
-            DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> {
-                // If task bounds are outside valid drag area, snap them inward and perform a
-                // transaction to set bounds.
-                if (
-                    DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
-                        taskBounds,
-                        validDragArea
-                    )
-                ) {
-                    val wct = WindowContainerTransaction()
-                    wct.setBounds(taskInfo.token, taskBounds)
-                    transitions.startTransition(TRANSIT_CHANGE, wct, null)
-                }
+            IndicatorType.NO_INDICATOR -> {
+                // If task bounds are outside valid drag area, snap them inward
+                DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
+                    currentDragBounds,
+                    validDragArea
+                )
+
+                // Update task bounds so that the task position will match the position of its leash
+                val wct = WindowContainerTransaction()
+                wct.setBounds(taskInfo.token, currentDragBounds)
+                transitions.startTransition(TRANSIT_CHANGE, wct, null)
+
                 releaseVisualIndicator()
             }
-            DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
+            IndicatorType.TO_DESKTOP_INDICATOR -> {
                 throw IllegalArgumentException(
                     "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task."
                 )
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 9e79eddb0..5221a45 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
@@ -29,10 +29,10 @@
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerToken
 import android.window.WindowContainerTransaction
-import com.android.internal.protolog.ProtoLog
 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
@@ -220,16 +220,18 @@
             startCancelAnimation()
         } else if (
             state.draggedTaskChange != null &&
-            (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+                (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
                     cancelState == CancelState.CANCEL_SPLIT_RIGHT)
-            ) {
+        ) {
             // We have a valid dragged task, but the animation will be handled by
             // SplitScreenController; request the transition here.
-            @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
-                SPLIT_POSITION_TOP_OR_LEFT
-            } else {
-                SPLIT_POSITION_BOTTOM_OR_RIGHT
-            }
+            @SplitPosition
+            val splitPosition =
+                if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+                    SPLIT_POSITION_TOP_OR_LEFT
+                } else {
+                    SPLIT_POSITION_BOTTOM_OR_RIGHT
+                }
             val wct = WindowContainerTransaction()
             restoreWindowOrder(wct, state)
             state.startTransitionFinishTransaction?.apply()
@@ -252,20 +254,20 @@
         wct: WindowContainerTransaction
     ) {
         val state = requireTransitionState()
-        val taskInfo = state.draggedTaskChange?.taskInfo
-            ?: error("Expected non-null taskInfo")
+        val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
         val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
         val taskScale = state.dragAnimator.scale
         val scaledWidth = taskBounds.width() * taskScale
         val scaledHeight = taskBounds.height() * taskScale
         val dragPosition = PointF(state.dragAnimator.position)
         state.dragAnimator.cancelAnimator()
-        val animatedTaskBounds = Rect(
-            dragPosition.x.toInt(),
-            dragPosition.y.toInt(),
-            (dragPosition.x + scaledWidth).toInt(),
-            (dragPosition.y + scaledHeight).toInt()
-        )
+        val animatedTaskBounds =
+            Rect(
+                dragPosition.x.toInt(),
+                dragPosition.y.toInt(),
+                (dragPosition.x + scaledWidth).toInt(),
+                (dragPosition.y + scaledHeight).toInt()
+            )
         requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
     }
 
@@ -286,12 +288,7 @@
         }
         wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
         wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
-        splitScreenController.requestEnterSplitSelect(
-            taskInfo,
-            wct,
-            splitPosition,
-            taskBounds
-        )
+        splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds)
     }
 
     override fun startAnimation(
@@ -390,7 +387,7 @@
                 // occurred.
                 if (
                     change.taskInfo?.taskId == state.draggedTaskId &&
-                    state.cancelState != CancelState.STANDARD_CANCEL
+                        state.cancelState != CancelState.STANDARD_CANCEL
                 ) {
                     // We need access to the dragged task's change in both non-cancel and split
                     // cancel cases.
@@ -398,8 +395,8 @@
                 }
                 if (
                     change.taskInfo?.taskId == state.draggedTaskId &&
-                    state.cancelState == CancelState.NO_CANCEL
-                    ) {
+                        state.cancelState == CancelState.NO_CANCEL
+                ) {
                     taskDisplayAreaOrganizer.reparentToDisplayArea(
                         change.endDisplayId,
                         change.leash,
@@ -432,19 +429,20 @@
             startCancelDragToDesktopTransition()
         } else if (
             state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
-            state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
-            ){
+                state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
+        ) {
             // Cancel-early case for split-cancel. The state was flagged already as a cancel for
             // requesting split select. Similar to the above, this can happen due to quick fling
             // gestures. We can simply request split here without needing to calculate animated
             // task bounds as the task has not shrunk at all.
-            val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
-                SPLIT_POSITION_TOP_OR_LEFT
-            } else {
-                SPLIT_POSITION_BOTTOM_OR_RIGHT
-            }
-            val taskInfo = state.draggedTaskChange?.taskInfo
-                ?: error("Expected non-null task info.")
+            val splitPosition =
+                if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+                    SPLIT_POSITION_TOP_OR_LEFT
+                } else {
+                    SPLIT_POSITION_BOTTOM_OR_RIGHT
+                }
+            val taskInfo =
+                state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.")
             val wct = WindowContainerTransaction()
             restoreWindowOrder(wct)
             state.startTransitionFinishTransaction?.apply()
@@ -463,8 +461,10 @@
     ) {
         val state = requireTransitionState()
         // We don't want to merge the split select animation if that's what we requested.
-        if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
-            state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) {
+        if (
+            state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+                state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
+        ) {
             clearState()
             return
         }
@@ -574,7 +574,8 @@
                                 startTransitionFinishCb.onTransitionFinished(null /* null */)
                                 clearState()
                                 interactionJankMonitor.end(
-                                    CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
+                                    CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
+                                )
                             }
                         }
                     )
@@ -673,9 +674,7 @@
         val wct = WindowContainerTransaction()
         restoreWindowOrder(wct, state)
         state.cancelTransitionToken =
-            transitions.startTransition(
-                TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this
-            )
+            transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
     }
 
     private fun restoreWindowOrder(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index c35d77a..bf185a4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -45,15 +45,24 @@
     private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
 
     private var boundsAnimator: Animator? = null
+    private var initialBounds: Rect? = null
 
     constructor(
         transitions: Transitions,
         interactionJankMonitor: InteractionJankMonitor
     ) : this(transitions, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor)
 
-    /** Starts a quick resize transition. */
-    fun startTransition(wct: WindowContainerTransaction) {
+    /**
+     * Starts a quick resize transition.
+     *
+     *  @param wct WindowContainerTransaction that will update core about the task changes applied
+     *  @param taskLeashBounds current bounds of the task leash (Note: not guaranteed to be the
+     *                         bounds of the actual task). This is provided so that the animation
+     *                         resizing can begin where the task leash currently is for smoother UX.
+     */
+    fun startTransition(wct: WindowContainerTransaction, taskLeashBounds: Rect? = null) {
         transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this)
+        initialBounds = taskLeashBounds
     }
 
     fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
@@ -70,7 +79,7 @@
         val change = findRelevantChange(info)
         val leash = change.leash
         val taskId = checkNotNull(change.taskInfo).taskId
-        val startBounds = change.startAbsBounds
+        val startBounds = initialBounds ?: change.startAbsBounds
         val endBounds = change.endAbsBounds
 
         val tx = transactionSupplier.get()
@@ -92,7 +101,7 @@
                             onTaskResizeAnimationListener.onAnimationStart(
                                 taskId,
                                 startTransaction,
-                                startBounds
+                                startBounds,
                             )
                         },
                         onEnd = {
@@ -106,6 +115,7 @@
                                 .show(leash)
                             onTaskResizeAnimationListener.onAnimationEnd(taskId)
                             finishCallback.onTransitionFinished(null)
+                            initialBounds = null
                             boundsAnimator = null
                             interactionJankMonitor.end(
                                 Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
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 54f908b..da7e03f 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
@@ -19,7 +19,6 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.pm.PackageManager.FEATURE_PC;
 
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
 
 import android.app.ActivityManager;
@@ -365,7 +364,7 @@
     private boolean shouldEnableRunningTasksForDesktopMode() {
         return mPcFeatureEnabled
                 || (DesktopModeStatus.canEnterDesktopMode(mContext)
-                && enableDesktopWindowingTaskbarRunningApps());
+                && DesktopModeFlags.TASKBAR_RUNNING_APPS.isEnabled(mContext));
     }
 
     @VisibleForTesting
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 234b4d0..ad3f4f8 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
@@ -19,12 +19,14 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_SLEEP;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
@@ -775,6 +777,20 @@
                     // Don't consider order-only & non-leaf changes as changing apps.
                     if (!TransitionUtil.isOrderOnly(change) && isLeafTask) {
                         hasChangingApp = true;
+                        // Check if the changing app is moving to top and fullscreen. This handles
+                        // the case where we moved from desktop to recents and launching a desktop
+                        // task in fullscreen.
+                        if ((change.getFlags() & FLAG_MOVED_TO_TOP) != 0
+                                && taskInfo != null
+                                && taskInfo.getWindowingMode()
+                                == WINDOWING_MODE_FULLSCREEN) {
+                            if (openingTasks == null) {
+                                openingTasks = new ArrayList<>();
+                                openingTaskIsLeafs = new IntArray();
+                            }
+                            openingTasks.add(change);
+                            openingTaskIsLeafs.add(1);
+                        }
                     } else if (isLeafTask && taskInfo.topActivityType == ACTIVITY_TYPE_HOME
                             && !isRecentsTask ) {
                         // Unless it is a 3p launcher. This means that the 3p launcher was already
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index b857556..c4af148 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -431,6 +432,20 @@
         mStageCoordinator.clearSplitPairedInRecents(reason);
     }
 
+    /**
+     * Determines which split position a new instance of a task should take.
+     * @param callingTask The task requesting a new instance.
+     * @return the split position of the new instance
+     */
+    public int determineNewInstancePosition(@NonNull ActivityManager.RunningTaskInfo callingTask) {
+        if (callingTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                || getSplitPosition(callingTask.taskId) == SPLIT_POSITION_TOP_OR_LEFT) {
+            return SPLIT_POSITION_BOTTOM_OR_RIGHT;
+        } else {
+            return SPLIT_POSITION_TOP_OR_LEFT;
+        }
+    }
+
     public void enterSplitScreen(int taskId, boolean leftOrTop) {
         enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction());
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 58cf398..c850ff8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -40,6 +40,7 @@
 
 import static com.android.window.flags.Flags.ensureWallpaperInTransitions;
 import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary;
+import static com.android.window.flags.Flags.migratePredictiveBackTransition;
 import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
 import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
@@ -821,7 +822,8 @@
             }
             // The change has already animated by back gesture, don't need to play transition
             // animation on it.
-            if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) {
+            if (!migratePredictiveBackTransition()
+                    && change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) {
                 info.getChanges().remove(i);
             }
         }
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 ecd5eda..d68c018 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
@@ -30,7 +30,6 @@
 import static android.view.MotionEvent.ACTION_HOVER_ENTER;
 import static android.view.MotionEvent.ACTION_HOVER_EXIT;
 import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_OUTSIDE;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.WindowInsets.Type.statusBars;
 
@@ -90,9 +89,11 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
+import com.android.wm.shell.common.DisplayChangeController;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.MultiInstanceHelper;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
@@ -117,6 +118,8 @@
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.Optional;
 import java.util.function.Supplier;
@@ -145,6 +148,7 @@
     private final DesktopTasksController mDesktopTasksController;
     private final InputManager mInputManager;
     private final InteractionJankMonitor mInteractionJankMonitor;
+    private final MultiInstanceHelper mMultiInstanceHelper;
     private boolean mTransitionDragActive;
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -172,6 +176,7 @@
     private boolean mInImmersiveMode;
     private final String mSysUIPackageName;
 
+    private final DisplayChangeController.OnDisplayChangingListener mOnDisplayChangingListener;
     private final ISystemGestureExclusionListener mGestureExclusionListener =
             new ISystemGestureExclusionListener.Stub() {
                 @Override
@@ -204,7 +209,8 @@
             Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             InteractionJankMonitor interactionJankMonitor,
-            AppToWebGenericLinksParser genericLinksParser
+            AppToWebGenericLinksParser genericLinksParser,
+            MultiInstanceHelper multiInstanceHelper
     ) {
         this(
                 context,
@@ -223,6 +229,7 @@
                 transitions,
                 desktopTasksController,
                 genericLinksParser,
+                multiInstanceHelper,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new,
@@ -249,6 +256,7 @@
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
             AppToWebGenericLinksParser genericLinksParser,
+            MultiInstanceHelper multiInstanceHelper,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -268,6 +276,7 @@
         mSyncQueue = syncQueue;
         mTransitions = transitions;
         mDesktopTasksController = desktopTasksController.get();
+        mMultiInstanceHelper = multiInstanceHelper;
         mShellCommandHandler = shellCommandHandler;
         mWindowManager = windowManager;
         mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
@@ -280,6 +289,31 @@
         mSysUIPackageName = mContext.getResources().getString(
                 com.android.internal.R.string.config_systemUi);
         mInteractionJankMonitor = interactionJankMonitor;
+        mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
+            DesktopModeWindowDecoration decoration;
+            RunningTaskInfo taskInfo;
+            for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
+                decoration = mWindowDecorByTaskId.valueAt(i);
+                if (decoration == null) {
+                    continue;
+                } else {
+                    taskInfo = decoration.mTaskInfo;
+                }
+
+                // Check if display has been rotated between portrait & landscape
+                if (displayId == taskInfo.displayId && taskInfo.isFreeform()
+                        && (fromRotation % 2 != toRotation % 2)) {
+                    // Check if the task bounds on the rotated display will be out of bounds.
+                    // If so, then update task bounds to be within reachable area.
+                    final Rect taskBounds = new Rect(
+                            taskInfo.configuration.windowConfiguration.getBounds());
+                    if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
+                            taskBounds, decoration.calculateValidDragArea())) {
+                        t.setBounds(taskInfo.token, taskBounds);
+                    }
+                }
+            }
+        };
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -290,7 +324,8 @@
         mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(),
                 new DesktopModeOnInsetsChangedListener());
         mDesktopTasksController.setOnTaskResizeAnimationListener(
-                new DeskopModeOnTaskResizeAnimationListener());
+                new DesktopModeOnTaskResizeAnimationListener());
+        mDisplayController.addDisplayChangingController(mOnDisplayChangingListener);
         try {
             mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
                     mContext.getDisplayId());
@@ -412,13 +447,13 @@
         mWindowDecorByTaskId.remove(taskInfo.taskId);
     }
 
-    private void onMaximizeOrRestore(int taskId, String tag) {
+    private void onMaximizeOrRestore(int taskId, String source) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
             return;
         }
         mInteractionJankMonitor.begin(
-                decoration.mTaskSurface, mContext, Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, tag);
+                decoration.mTaskSurface, mContext, Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
         mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
@@ -430,15 +465,20 @@
             return;
         }
         mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo,
+                decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
                 left ? SnapPosition.LEFT : SnapPosition.RIGHT);
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
     }
 
-    private void onOpenInBrowser(@NonNull DesktopModeWindowDecoration decor, @NonNull Uri uri) {
+    private void onOpenInBrowser(int taskId, @NonNull Uri uri) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
         openInBrowser(uri);
-        decor.closeHandleMenu();
-        decor.closeMaximizeMenu();
+        decoration.closeHandleMenu();
+        decoration.closeMaximizeMenu();
     }
 
     private void openInBrowser(Uri uri) {
@@ -448,6 +488,57 @@
         mContext.startActivity(intent);
     }
 
+    private void onToDesktop(int taskId, DesktopModeTransitionSource source) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
+                CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU);
+        // App sometimes draws before the insets from WindowDecoration#relayout have
+        // been added, so they must be added here
+        decoration.addCaptionInset(wct);
+        mDesktopTasksController.moveTaskToDesktop(taskId, wct, source);
+        decoration.closeHandleMenu();
+    }
+
+    private void onToFullscreen(int taskId) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        decoration.closeHandleMenu();
+        if (isTaskInSplitScreen(taskId)) {
+            mSplitScreenController.moveTaskToFullscreen(taskId,
+                    SplitScreenController.EXIT_REASON_DESKTOP_MODE);
+        } else {
+            mDesktopTasksController.moveToFullscreen(taskId,
+                    DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
+        }
+    }
+
+    private void onToSplitScreen(int taskId) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        decoration.closeHandleMenu();
+        // When the app enters split-select, the handle will no longer be visible, meaning
+        // we shouldn't receive input for it any longer.
+        decoration.disposeStatusBarInputLayer();
+        mDesktopTasksController.requestSplit(decoration.mTaskInfo, false /* leftOrTop */);
+    }
+
+    private void onNewWindow(int taskId) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        decoration.closeHandleMenu();
+        mDesktopTasksController.openNewWindow(decoration.mTaskInfo);
+    }
+
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
@@ -505,37 +596,6 @@
                     moveTaskToFront(decoration.mTaskInfo);
                     decoration.createHandleMenu(mSplitScreenController);
                 }
-            } else if (id == R.id.desktop_button) {
-                final WindowContainerTransaction wct = new WindowContainerTransaction();
-                // App sometimes draws before the insets from WindowDecoration#relayout have
-                // been added, so they must be added here
-                mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
-                        CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU);
-                mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
-                mDesktopTasksController.moveTaskToDesktop(mTaskId, wct,
-                        DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
-                decoration.closeHandleMenu();
-            } else if (id == R.id.fullscreen_button) {
-                decoration.closeHandleMenu();
-                if (isTaskInSplitScreen(mTaskId)) {
-                    mSplitScreenController.moveTaskToFullscreen(mTaskId,
-                            SplitScreenController.EXIT_REASON_DESKTOP_MODE);
-                } else {
-                    mDesktopTasksController.moveToFullscreen(mTaskId,
-                            DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
-                }
-            } else if (id == R.id.split_screen_button) {
-                decoration.closeHandleMenu();
-                // When the app enters split-select, the handle will no longer be visible, meaning
-                // we shouldn't receive input for it any longer.
-                decoration.disposeStatusBarInputLayer();
-                mDesktopTasksController.requestSplit(decoration.mTaskInfo);
-            } else if (id == R.id.open_in_browser_button) {
-                // TODO(b/346441962): let the decoration handle the click gesture and only call back
-                //  to the ViewModel via #setOpenInBrowserClickListener
-                decoration.onOpenInBrowserClick();
-            } else if (id == R.id.collapse_menu_button) {
-                decoration.closeHandleMenu();
             } else if (id == R.id.maximize_window) {
                 // TODO(b/346441962): move click detection logic into the decor's
                 //  {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report
@@ -550,16 +610,6 @@
         public boolean onTouch(View v, MotionEvent e) {
             final int id = v.getId();
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
-            if (e.getActionMasked() == ACTION_OUTSIDE) {
-                if (id == R.id.handle_menu) {
-                    // Close handle menu on outside touch if menu is directly touchable; if not,
-                    // it will be handled by handleEventOutsideCaption.
-                    if (decoration.mTaskInfo.isFreeform()
-                            || Flags.enableAdditionalWindowsAboveStatusBar()) {
-                        decoration.closeHandleMenu();
-                    }
-                }
-            }
             if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) {
                 mTouchscreenInUse = e.getActionMasked() != ACTION_UP
                         && e.getActionMasked() != ACTION_CANCEL;
@@ -746,6 +796,9 @@
                             (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx)));
                     final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+                    // Tasks bounds haven't actually been updated (only its leash), so pass to
+                    // DesktopTasksController to allow secondary transformations (i.e. snap resizing
+                    // or transforming to fullscreen) before setting new task bounds.
                     mDesktopTasksController.onDragPositioningEnd(taskInfo, position,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                             newTaskBounds, decoration.calculateValidDragArea());
@@ -1161,18 +1214,19 @@
                         mMainChoreographer,
                         mSyncQueue,
                         mRootTaskDisplayAreaOrganizer,
-                        mGenericLinksParser);
+                        mGenericLinksParser,
+                        mMultiInstanceHelper);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
         final DragPositioningCallback dragPositioningCallback;
         if (!DesktopModeStatus.isVeiledResizeEnabled()) {
-            dragPositioningCallback =  new FluidResizeTaskPositioner(
+            dragPositioningCallback = new FluidResizeTaskPositioner(
                     mTaskOrganizer, mTransitions, windowDecoration, mDisplayController,
                     mDragStartListener, mTransactionFactory);
             windowDecoration.setTaskDragResizer(
                     (FluidResizeTaskPositioner) dragPositioningCallback);
         } else {
-            dragPositioningCallback =  new VeiledResizeTaskPositioner(
+            dragPositioningCallback = new VeiledResizeTaskPositioner(
                     mTaskOrganizer, windowDecoration, mDisplayController,
                     mDragStartListener, mTransitions, mInteractionJankMonitor);
             windowDecoration.setTaskDragResizer(
@@ -1181,14 +1235,36 @@
 
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
-        windowDecoration.setOnMaximizeOrRestoreClickListener(this::onMaximizeOrRestore);
-        windowDecoration.setOnLeftSnapClickListener((taskId, tag) -> {
-            onSnapResize(taskId, true /* isLeft */);
+        windowDecoration.setOnMaximizeOrRestoreClickListener(() -> {
+            onMaximizeOrRestore(taskInfo.taskId, "maximize_menu");
+            return Unit.INSTANCE;
         });
-        windowDecoration.setOnRightSnapClickListener((taskId, tag) -> {
-            onSnapResize(taskId, false /* isLeft */);
+        windowDecoration.setOnLeftSnapClickListener(() -> {
+            onSnapResize(taskInfo.taskId, true /* isLeft */);
+            return Unit.INSTANCE;
         });
-        windowDecoration.setOpenInBrowserClickListener(this::onOpenInBrowser);
+        windowDecoration.setOnRightSnapClickListener(() -> {
+            onSnapResize(taskInfo.taskId, false /* isLeft */);
+            return Unit.INSTANCE;
+        });
+        windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> {
+            onToDesktop(taskInfo.taskId, desktopModeTransitionSource);
+        });
+        windowDecoration.setOnToFullscreenClickListener(() -> {
+            onToFullscreen(taskInfo.taskId);
+            return Unit.INSTANCE;
+        });
+        windowDecoration.setOnToSplitScreenClickListener(() -> {
+            onToSplitScreen(taskInfo.taskId);
+            return Unit.INSTANCE;
+        });
+        windowDecoration.setOpenInBrowserClickListener((uri) -> {
+            onOpenInBrowser(taskInfo.taskId, uri);
+        });
+        windowDecoration.setOnNewWindowClickListener(() -> {
+            onNewWindow(taskInfo.taskId);
+            return Unit.INSTANCE;
+        });
         windowDecoration.setCaptionListeners(
                 touchEventListener, touchEventListener, touchEventListener, touchEventListener);
         windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
@@ -1223,7 +1299,7 @@
         pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
     }
 
-    private class DeskopModeOnTaskResizeAnimationListener
+    private class DesktopModeOnTaskResizeAnimationListener
             implements OnTaskResizeAnimationListener {
         @Override
         public void onAnimationStart(int taskId, Transaction t, Rect bounds) {
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 df41d31..d70e225 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
@@ -26,6 +26,7 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
@@ -74,20 +75,23 @@
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.MultiInstanceHelper;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder;
 
 import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
 
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -114,9 +118,13 @@
     private View.OnTouchListener mOnCaptionTouchListener;
     private View.OnLongClickListener mOnCaptionLongClickListener;
     private View.OnGenericMotionListener mOnCaptionGenericMotionListener;
-    private OnTaskActionClickListener mOnMaximizeOrRestoreClickListener;
-    private OnTaskActionClickListener mOnLeftSnapClickListener;
-    private OnTaskActionClickListener mOnRightSnapClickListener;
+    private Function0<Unit> mOnMaximizeOrRestoreClickListener;
+    private Function0<Unit> mOnLeftSnapClickListener;
+    private Function0<Unit> mOnRightSnapClickListener;
+    private Consumer<DesktopModeTransitionSource> mOnToDesktopClickListener;
+    private Function0<Unit> mOnToFullscreenClickListener;
+    private Function0<Unit> mOnToSplitscreenClickListener;
+    private Function0<Unit> mOnNewWindowClickListener;
     private DragPositioningCallback mDragPositioningCallback;
     private DragResizeInputListener mDragResizeListener;
     private DragDetector mDragDetector;
@@ -139,7 +147,7 @@
     private CharSequence mAppName;
     private CapturedLink mCapturedLink;
     private Uri mGenericLink;
-    private OpenInBrowserClickListener mOpenInBrowserClickListener;
+    private Consumer<Uri> mOpenInBrowserClickListener;
 
     private ExclusionRegionListener mExclusionRegionListener;
 
@@ -157,6 +165,7 @@
     // to cancel the close.
     private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
     private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
+    private final MultiInstanceHelper mMultiInstanceHelper;
 
     DesktopModeWindowDecoration(
             Context context,
@@ -171,13 +180,15 @@
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            AppToWebGenericLinksParser genericLinksParser) {
+            AppToWebGenericLinksParser genericLinksParser,
+            MultiInstanceHelper multiInstanceHelper) {
         this (context, userContext, displayController, splitScreenController, taskOrganizer,
                 taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
                 rootTaskDisplayAreaOrganizer, genericLinksParser, SurfaceControl.Builder::new,
                 SurfaceControl.Transaction::new,  WindowContainerTransaction::new,
                 SurfaceControl::new, new SurfaceControlViewHostFactory() {},
-                DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE);
+                DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE,
+                multiInstanceHelper);
     }
 
     DesktopModeWindowDecoration(
@@ -200,7 +211,8 @@
             Supplier<SurfaceControl> surfaceControlSupplier,
             SurfaceControlViewHostFactory surfaceControlViewHostFactory,
             MaximizeMenuFactory maximizeMenuFactory,
-            HandleMenuFactory handleMenuFactory) {
+            HandleMenuFactory handleMenuFactory,
+            MultiInstanceHelper multiInstanceHelper) {
         super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -214,6 +226,7 @@
         mGenericLinksParser = genericLinksParser;
         mMaximizeMenuFactory = maximizeMenuFactory;
         mHandleMenuFactory = handleMenuFactory;
+        mMultiInstanceHelper = multiInstanceHelper;
     }
 
     /**
@@ -222,24 +235,42 @@
      * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of
      *  having the ViewModel deal with parsing motion events.
      */
-    void setOnMaximizeOrRestoreClickListener(OnTaskActionClickListener listener) {
+    void setOnMaximizeOrRestoreClickListener(Function0<Unit> listener) {
         mOnMaximizeOrRestoreClickListener = listener;
     }
 
-    /**
-     * Register a listener to be called back when one of the tasks snap-left action is triggered.
-     */
-    void setOnLeftSnapClickListener(OnTaskActionClickListener listener) {
+    /** Registers a listener to be called when the decoration's snap-left action is triggered.*/
+    void setOnLeftSnapClickListener(Function0<Unit> listener) {
         mOnLeftSnapClickListener = listener;
     }
 
-    /**
-     * Register a listener to be called back when one of the tasks' snap-right action is triggered.
-     */
-    void setOnRightSnapClickListener(OnTaskActionClickListener listener) {
+    /** Registers a listener to be called when the decoration's snap-right action is triggered. */
+    void setOnRightSnapClickListener(Function0<Unit> listener) {
         mOnRightSnapClickListener = listener;
     }
 
+    /** Registers a listener to be called when the decoration's to-desktop action is triggered. */
+    void setOnToDesktopClickListener(Consumer<DesktopModeTransitionSource> listener) {
+        mOnToDesktopClickListener = listener;
+    }
+
+    /**
+     * Registers a listener to be called when the decoration's to-fullscreen action is triggered.
+     */
+    void setOnToFullscreenClickListener(Function0<Unit> listener) {
+        mOnToFullscreenClickListener = listener;
+    }
+
+    /** Registers a listener to be called when the decoration's to-split action is triggered. */
+    void setOnToSplitScreenClickListener(Function0<Unit> listener) {
+        mOnToSplitscreenClickListener = listener;
+    }
+
+    /** Registers a listener to be called when the decoration's new window action is triggered. */
+    void setOnNewWindowClickListener(Function0<Unit> listener) {
+        mOnNewWindowClickListener = listener;
+    }
+
     void setCaptionListeners(
             View.OnClickListener onCaptionButtonClickListener,
             View.OnTouchListener onCaptionTouchListener,
@@ -264,7 +295,7 @@
         mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
     }
 
-    void setOpenInBrowserClickListener(OpenInBrowserClickListener listener) {
+    void setOpenInBrowserClickListener(Consumer<Uri> listener) {
         mOpenInBrowserClickListener = listener;
     }
 
@@ -281,8 +312,7 @@
         // transaction (that applies task crop) is synced with the buffer transaction (that draws
         // the View). Both will be shown on screen at the same, whereas applying them independently
         // causes flickering. See b/270202228.
-        final boolean applyTransactionOnDraw =
-                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        final boolean applyTransactionOnDraw = taskInfo.isFreeform();
         relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop);
         if (!applyTransactionOnDraw) {
             t.apply();
@@ -293,7 +323,7 @@
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
         Trace.beginSection("DesktopModeWindowDecoration#relayout");
-        if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+        if (taskInfo.isFreeform()) {
             // The Task is in Freeform mode -> show its header in sync since it's an integral part
             // of the window itself - a delayed header might cause bad UX.
             relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
@@ -439,14 +469,6 @@
         }
     }
 
-    void onOpenInBrowserClick() {
-        if (mOpenInBrowserClickListener == null || mHandleMenu == null) {
-            return;
-        }
-        mOpenInBrowserClickListener.onClick(this, mHandleMenu.getOpenInBrowserLink());
-        onCapturedLinkExpired();
-    }
-
     @Nullable
     private Uri getBrowserLink() {
         // If the captured link is available and has not expired, return the captured link.
@@ -501,9 +523,7 @@
     }
 
     private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
-        final boolean isFreeform =
-                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
-        return isFreeform && taskInfo.isResizeable;
+        return taskInfo.isFreeform() && taskInfo.isResizeable;
     }
 
     private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
@@ -959,20 +979,41 @@
         mHandleMenu = mHandleMenuFactory.create(
                 this,
                 mRelayoutParams.mLayoutResId,
-                mOnCaptionButtonClickListener,
-                mOnCaptionTouchListener,
                 mAppIconBitmap,
                 mAppName,
-                mDisplayController,
                 splitScreenController,
                 DesktopModeStatus.canEnterDesktopMode(mContext),
+                Flags.enableDesktopWindowingMultiInstanceFeatures()
+                        && mMultiInstanceHelper
+                        .supportsMultiInstanceSplit(mTaskInfo.baseActivity),
                 getBrowserLink(),
                 mResult.mCaptionWidth,
                 mResult.mCaptionHeight,
                 mResult.mCaptionX
         );
         mWindowDecorViewHolder.onHandleMenuOpened();
-        mHandleMenu.show();
+        mHandleMenu.show(
+                /* onToDesktopClickListener= */ () -> {
+                    mOnToDesktopClickListener.accept(APP_HANDLE_MENU_BUTTON);
+                    return Unit.INSTANCE;
+                },
+                /* onToFullscreenClickListener= */ mOnToFullscreenClickListener,
+                /* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener,
+                /* onNewWindowClickListener= */ mOnNewWindowClickListener,
+                /* openInBrowserClickListener= */ (uri) -> {
+                    mOpenInBrowserClickListener.accept(uri);
+                    onCapturedLinkExpired();
+                    return Unit.INSTANCE;
+                },
+                /* onCloseMenuClickListener= */ () -> {
+                    closeHandleMenu();
+                    return Unit.INSTANCE;
+                },
+                /* onOutsideTouchListener= */ () -> {
+                    closeHandleMenu();
+                    return Unit.INSTANCE;
+                }
+        );
     }
 
     private void updateGenericLink() {
@@ -1274,7 +1315,8 @@
                 Choreographer choreographer,
                 SyncTransactionQueue syncQueue,
                 RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-                AppToWebGenericLinksParser genericLinksParser) {
+                AppToWebGenericLinksParser genericLinksParser,
+                MultiInstanceHelper multiInstanceHelper) {
             return new DesktopModeWindowDecoration(
                     context,
                     userContext,
@@ -1288,7 +1330,8 @@
                     choreographer,
                     syncQueue,
                     rootTaskDisplayAreaOrganizer,
-                    genericLinksParser);
+                    genericLinksParser,
+                    multiInstanceHelper);
         }
     }
 
@@ -1309,14 +1352,6 @@
         }
     }
 
-
-    /** Listener for the handle menu's "Open in browser" button */
-    interface OpenInBrowserClickListener {
-
-        /** Inform the implementing class that the "Open in browser" button has been clicked */
-        void onClick(DesktopModeWindowDecoration decoration, Uri uri);
-    }
-
     interface ExclusionRegionListener {
         /** Inform the implementing class of this task's change in region resize handles */
         void onExclusionRegionChanged(int taskId, Region region);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 76096b0..e2d42b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -152,11 +152,8 @@
             }
             mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
         } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
                     mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
-            wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
-            mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
         }
 
         mTaskBoundsAtDragStart.setEmpty();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 7a81d4c..b348d65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -17,7 +17,8 @@
 
 import android.annotation.ColorInt
 import android.annotation.DimenRes
-import android.app.ActivityManager
+import android.annotation.SuppressLint
+import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.Resources
@@ -28,7 +29,9 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.net.Uri
+import android.view.LayoutInflater
 import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_OUTSIDE
 import android.view.SurfaceControl
 import android.view.View
 import android.view.WindowManager
@@ -42,7 +45,6 @@
 import androidx.core.view.isGone
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
-import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.split.SplitScreenConstants
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -63,21 +65,18 @@
 class HandleMenu(
     private val parentDecor: DesktopModeWindowDecoration,
     private val layoutResId: Int,
-    private val onClickListener: View.OnClickListener?,
-    private val onTouchListener: View.OnTouchListener?,
     private val appIconBitmap: Bitmap?,
     private val appName: CharSequence?,
-    private val displayController: DisplayController,
     private val splitScreenController: SplitScreenController,
     private val shouldShowWindowingPill: Boolean,
-    val openInBrowserLink: Uri?,
+    private val shouldShowNewWindowButton: Boolean,
+    private val openInBrowserLink: Uri?,
     private val captionWidth: Int,
     private val captionHeight: Int,
     captionX: Int
 ) {
     private val context: Context = parentDecor.mDecorWindowContext
-    private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo
-    private val decorThemeUtil = DecorThemeUtil(context)
+    private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo
 
     private val isViewAboveStatusBar: Boolean
         get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform)
@@ -93,10 +92,9 @@
     private val marginMenuStart = loadDimensionPixelSize(
         R.dimen.desktop_mode_handle_menu_margin_start)
 
-    private var handleMenuAnimator: HandleMenuAnimator? = null
-
     @VisibleForTesting
     var handleMenuViewContainer: AdditionalViewContainer? = null
+    private var handleMenuView: HandleMenuView? = null
 
     // Position of the handle menu used for laying out the handle view.
     @VisibleForTesting
@@ -115,157 +113,88 @@
         updateHandleMenuPillPositions(captionX)
     }
 
-    fun show() {
+    fun show(
+        onToDesktopClickListener: () -> Unit,
+        onToFullscreenClickListener: () -> Unit,
+        onToSplitScreenClickListener: () -> Unit,
+        onNewWindowClickListener: () -> Unit,
+        openInBrowserClickListener: (Uri) -> Unit,
+        onCloseMenuClickListener: () -> Unit,
+        onOutsideTouchListener: () -> Unit,
+    ) {
         val ssg = SurfaceSyncGroup(TAG)
         val t = SurfaceControl.Transaction()
 
-        createHandleMenuViewContainer(t, ssg)
+        createHandleMenu(
+            t = t,
+            ssg = ssg,
+            onToDesktopClickListener = onToDesktopClickListener,
+            onToFullscreenClickListener = onToFullscreenClickListener,
+            onToSplitScreenClickListener = onToSplitScreenClickListener,
+            onNewWindowClickListener = onNewWindowClickListener,
+            openInBrowserClickListener = openInBrowserClickListener,
+            onCloseMenuClickListener = onCloseMenuClickListener,
+            onOutsideTouchListener = onOutsideTouchListener,
+        )
         ssg.addTransaction(t)
         ssg.markSyncReady()
-        setupHandleMenu()
-        animateHandleMenu()
+
+        handleMenuView?.animateOpenMenu()
     }
 
-    private fun createHandleMenuViewContainer(
+    private fun createHandleMenu(
         t: SurfaceControl.Transaction,
-        ssg: SurfaceSyncGroup
+        ssg: SurfaceSyncGroup,
+        onToDesktopClickListener: () -> Unit,
+        onToFullscreenClickListener: () -> Unit,
+        onToSplitScreenClickListener: () -> Unit,
+        onNewWindowClickListener: () -> Unit,
+        openInBrowserClickListener: (Uri) -> Unit,
+        onCloseMenuClickListener: () -> Unit,
+        onOutsideTouchListener: () -> Unit
     ) {
+        val handleMenuView = HandleMenuView(
+            context = context,
+            menuWidth = menuWidth,
+            captionHeight = captionHeight,
+            shouldShowWindowingPill = shouldShowWindowingPill,
+            shouldShowBrowserPill = shouldShowBrowserPill,
+            shouldShowNewWindowButton = shouldShowNewWindowButton
+        ).apply {
+            bind(taskInfo, appIconBitmap, appName)
+            this.onToDesktopClickListener = onToDesktopClickListener
+            this.onToFullscreenClickListener = onToFullscreenClickListener
+            this.onToSplitScreenClickListener = onToSplitScreenClickListener
+            this.onNewWindowClickListener = onNewWindowClickListener
+            this.onOpenInBrowserClickListener = {
+                openInBrowserClickListener.invoke(openInBrowserLink!!)
+            }
+            this.onCloseMenuClickListener = onCloseMenuClickListener
+            this.onOutsideTouchListener = onOutsideTouchListener
+        }
+
         val x = handleMenuPosition.x.toInt()
         val y = handleMenuPosition.y.toInt()
         handleMenuViewContainer =
             if (!taskInfo.isFreeform && Flags.enableAdditionalWindowsAboveStatusBar()) {
                 AdditionalSystemViewContainer(
                     context = context,
-                    layoutId = R.layout.desktop_mode_window_decor_handle_menu,
                     taskId = taskInfo.taskId,
                     x = x,
                     y = y,
                     width = menuWidth,
                     height = menuHeight,
                     flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
-                        WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                            WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+                    view = handleMenuView.rootView
                 )
             } else {
                 parentDecor.addWindow(
-                    R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu",
-                    t, ssg, x, y, menuWidth, menuHeight
+                    handleMenuView.rootView, "Handle Menu", t, ssg, x, y, menuWidth, menuHeight
                 )
             }
-        handleMenuViewContainer?.view?.let { view ->
-            handleMenuAnimator =
-                HandleMenuAnimator(view, menuWidth, captionHeight.toFloat())
-        }
-    }
 
-    /**
-     * Animates the appearance of the handle menu and its three pills.
-     */
-    private fun animateHandleMenu() {
-        when {
-            taskInfo.isFullscreen || taskInfo.isMultiWindow -> {
-                handleMenuAnimator?.animateCaptionHandleExpandToOpen()
-            }
-            else -> {
-                handleMenuAnimator?.animateOpen()
-            }
-        }
-    }
-
-    /**
-     * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions
-     * pill.
-     */
-    private fun setupHandleMenu() {
-        val handleMenu = handleMenuViewContainer?.view ?: return
-        handleMenu.setOnTouchListener(onTouchListener)
-
-        val style = calculateMenuStyle()
-        setupAppInfoPill(handleMenu, style)
-        if (shouldShowWindowingPill) {
-            setupWindowingPill(handleMenu, style)
-        }
-        setupMoreActionsPill(handleMenu, style)
-        setupOpenInBrowserPill(handleMenu, style)
-    }
-
-    /**
-     * Set up interactive elements of handle menu's app info pill.
-     */
-    private fun setupAppInfoPill(handleMenu: View, style: MenuStyle) {
-        val pill = handleMenu.requireViewById<View>(R.id.app_info_pill).apply {
-            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
-        }
-
-        pill.requireViewById<HandleMenuImageButton>(R.id.collapse_menu_button)
-            .let { collapseBtn ->
-                collapseBtn.imageTintList = ColorStateList.valueOf(style.textColor)
-                collapseBtn.setOnClickListener(onClickListener)
-                collapseBtn.taskInfo = taskInfo
-            }
-        pill.requireViewById<ImageView>(R.id.application_icon).let { appIcon ->
-            appIcon.setImageBitmap(appIconBitmap)
-        }
-        pill.requireViewById<TextView>(R.id.application_name).let { appNameView ->
-            appNameView.text = appName
-            appNameView.setTextColor(style.textColor)
-        }
-    }
-
-    /**
-     * Set up interactive elements and color of handle menu's windowing pill.
-     */
-    private fun setupWindowingPill(handleMenu: View, style: MenuStyle) {
-        val pill = handleMenu.requireViewById<View>(R.id.windowing_pill).apply {
-            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
-        }
-
-        val fullscreenBtn = pill.requireViewById<ImageButton>(R.id.fullscreen_button)
-        val splitscreenBtn = pill.requireViewById<ImageButton>(R.id.split_screen_button)
-        val floatingBtn = pill.requireViewById<ImageButton>(R.id.floating_button)
-        // TODO: Remove once implemented.
-        floatingBtn.visibility = View.GONE
-        val desktopBtn = handleMenu.requireViewById<ImageButton>(R.id.desktop_button)
-
-        fullscreenBtn.setOnClickListener(onClickListener)
-        splitscreenBtn.setOnClickListener(onClickListener)
-        floatingBtn.setOnClickListener(onClickListener)
-        desktopBtn.setOnClickListener(onClickListener)
-
-        fullscreenBtn.isSelected = taskInfo.isFullscreen
-        fullscreenBtn.imageTintList = style.windowingButtonColor
-        splitscreenBtn.isSelected = taskInfo.isMultiWindow
-        splitscreenBtn.imageTintList = style.windowingButtonColor
-        floatingBtn.isSelected = taskInfo.isPinned
-        floatingBtn.imageTintList = style.windowingButtonColor
-        desktopBtn.isSelected = taskInfo.isFreeform
-        desktopBtn.imageTintList = style.windowingButtonColor
-    }
-
-    /**
-     * Set up interactive elements & height of handle menu's more actions pill
-     */
-    private fun setupMoreActionsPill(handleMenu: View, style: MenuStyle) {
-        val pill = handleMenu.requireViewById<View>(R.id.more_actions_pill).apply {
-            isGone = !SHOULD_SHOW_MORE_ACTIONS_PILL
-            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
-        }
-        pill.requireViewById<Button>(R.id.screenshot_button).let { screenshotBtn ->
-            screenshotBtn.setTextColor(style.textColor)
-            screenshotBtn.compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
-        }
-    }
-
-    private fun setupOpenInBrowserPill(handleMenu: View, style: MenuStyle) {
-        val pill = handleMenu.requireViewById<View>(R.id.open_in_browser_pill).apply {
-            isGone = !shouldShowBrowserPill
-            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
-        }
-
-        pill.requireViewById<Button>(R.id.open_in_browser_button).let { browserButton ->
-            browserButton.setOnClickListener(onClickListener)
-            browserButton.setTextColor(style.textColor)
-            browserButton.compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
-        }
+        this.handleMenuView = handleMenuView
     }
 
     /**
@@ -359,16 +288,8 @@
     fun checkMotionEvent(ev: MotionEvent) {
         // If the menu view is above status bar, we can let the views handle input directly.
         if (isViewAboveStatusBar) return
-        val handleMenu = handleMenuViewContainer?.view ?: return
-        val collapse = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button)
         val inputPoint = translateInputToLocalSpace(ev)
-        val inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y)
-        val action = ev.actionMasked
-        collapse.isHovered = inputInCollapseButton && action != MotionEvent.ACTION_UP
-        collapse.isPressed = inputInCollapseButton && action == MotionEvent.ACTION_DOWN
-        if (action == MotionEvent.ACTION_UP && inputInCollapseButton) {
-            collapse.performClick()
-        }
+        handleMenuView?.checkMotionEvent(ev, inputPoint)
     }
 
     // Translate the input point from display coordinates to the same space as the handle menu.
@@ -439,9 +360,17 @@
                 R.dimen.desktop_mode_handle_menu_windowing_pill_height)
             menuHeight -= pillTopMargin
         }
-        if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
+        if (!SHOULD_SHOW_SCREENSHOT_BUTTON) {
             menuHeight -= loadDimensionPixelSize(
-                R.dimen.desktop_mode_handle_menu_more_actions_pill_height)
+                R.dimen.desktop_mode_handle_menu_screenshot_height
+            )
+        }
+        if (!shouldShowNewWindowButton) {
+            menuHeight -= loadDimensionPixelSize(
+                R.dimen.desktop_mode_handle_menu_new_window_height
+            )
+        }
+        if (!SHOULD_SHOW_SCREENSHOT_BUTTON && !shouldShowNewWindowButton) {
             menuHeight -= pillTopMargin
         }
         if (!shouldShowBrowserPill) {
@@ -460,48 +389,244 @@
     }
 
     fun close() {
-        val after = {
+        handleMenuView?.animateCloseMenu {
             handleMenuViewContainer?.releaseView()
             handleMenuViewContainer = null
         }
-        if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
-            handleMenuAnimator?.animateCollapseIntoHandleClose(after)
-        } else {
-            handleMenuAnimator?.animateClose(after)
-        }
     }
 
-    private fun calculateMenuStyle(): MenuStyle {
-        val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
-        return MenuStyle(
-            backgroundColor = colorScheme.surfaceBright.toArgb(),
-            textColor = colorScheme.onSurface.toArgb(),
-            windowingButtonColor = ColorStateList(
-                arrayOf(
-                    intArrayOf(android.R.attr.state_pressed),
-                    intArrayOf(android.R.attr.state_focused),
-                    intArrayOf(android.R.attr.state_selected),
-                    intArrayOf(),
+    /** The view within the Handle Menu, with options to change the windowing mode and more. */
+    @SuppressLint("ClickableViewAccessibility")
+    class HandleMenuView(
+        context: Context,
+        menuWidth: Int,
+        captionHeight: Int,
+        private val shouldShowWindowingPill: Boolean,
+        private val shouldShowBrowserPill: Boolean,
+        private val shouldShowNewWindowButton: Boolean
+    ) {
+        val rootView = LayoutInflater.from(context)
+            .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
+
+        // App Info Pill.
+        private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
+        private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>(
+            R.id.collapse_menu_button)
+        private val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon)
+        private val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name)
+
+        // Windowing Pill.
+        private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill)
+        private val fullscreenBtn = windowingPill.requireViewById<ImageButton>(
+            R.id.fullscreen_button)
+        private val splitscreenBtn = windowingPill.requireViewById<ImageButton>(
+            R.id.split_screen_button)
+        private val floatingBtn = windowingPill.requireViewById<ImageButton>(R.id.floating_button)
+        private val desktopBtn = windowingPill.requireViewById<ImageButton>(R.id.desktop_button)
+
+        // More Actions Pill.
+        private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill)
+        private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button)
+        private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button)
+
+        // Open in Browser Pill.
+        private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill)
+        private val browserBtn = openInBrowserPill.requireViewById<Button>(
+            R.id.open_in_browser_button)
+
+        private val decorThemeUtil = DecorThemeUtil(context)
+        private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat())
+
+        private lateinit var taskInfo: RunningTaskInfo
+        private lateinit var style: MenuStyle
+
+        var onToDesktopClickListener: (() -> Unit)? = null
+        var onToFullscreenClickListener: (() -> Unit)? = null
+        var onToSplitScreenClickListener: (() -> Unit)? = null
+        var onNewWindowClickListener: (() -> Unit)? = null
+        var onOpenInBrowserClickListener: (() -> Unit)? = null
+        var onCloseMenuClickListener: (() -> Unit)? = null
+        var onOutsideTouchListener: (() -> Unit)? = null
+
+        init {
+            fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() }
+            splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() }
+            desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() }
+            browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() }
+            collapseMenuButton.setOnClickListener { onCloseMenuClickListener?.invoke() }
+            newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() }
+
+            rootView.setOnTouchListener { _, event ->
+                if (event.actionMasked == ACTION_OUTSIDE) {
+                    onOutsideTouchListener?.invoke()
+                    return@setOnTouchListener false
+                }
+                return@setOnTouchListener true
+            }
+        }
+
+        /** Binds the menu views to the new data. */
+        fun bind(taskInfo: RunningTaskInfo, appIconBitmap: Bitmap?, appName: CharSequence?) {
+            this.taskInfo = taskInfo
+            this.style = calculateMenuStyle(taskInfo)
+
+            bindAppInfoPill(style, appIconBitmap, appName)
+            if (shouldShowWindowingPill) {
+                bindWindowingPill(style)
+            }
+            bindMoreActionsPill(style)
+            bindOpenInBrowserPill(style)
+        }
+
+        /** Animates the menu opening. */
+        fun animateOpenMenu() {
+            if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
+                animator.animateCaptionHandleExpandToOpen()
+            } else {
+                animator.animateOpen()
+            }
+        }
+
+        /** Animates the menu closing. */
+        fun animateCloseMenu(onAnimFinish: () -> Unit) {
+            if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
+                animator.animateCollapseIntoHandleClose(onAnimFinish)
+            } else {
+                animator.animateClose(onAnimFinish)
+            }
+        }
+
+        /**
+         * Checks whether a motion event falls inside this menu, and invokes a click of the
+         * collapse button if needed.
+         * Note: should only be called when regular click detection doesn't work because input is
+         * detected through the status bar layer with a global input monitor.
+         */
+        fun checkMotionEvent(ev: MotionEvent, inputPointLocal: PointF) {
+            val inputInCollapseButton = pointInView(
+                collapseMenuButton,
+                inputPointLocal.x,
+                inputPointLocal.y
+            )
+            val action = ev.actionMasked
+            collapseMenuButton.isHovered = inputInCollapseButton
+                    && action != MotionEvent.ACTION_UP
+            collapseMenuButton.isPressed = inputInCollapseButton
+                    && action == MotionEvent.ACTION_DOWN
+            if (action == MotionEvent.ACTION_UP && inputInCollapseButton) {
+                collapseMenuButton.performClick()
+            }
+        }
+
+        private fun pointInView(v: View?, x: Float, y: Float): Boolean {
+            return v != null && v.left <= x && v.right >= x && v.top <= y && v.bottom >= y
+        }
+
+        private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle {
+            val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
+            return MenuStyle(
+                backgroundColor = colorScheme.surfaceBright.toArgb(),
+                textColor = colorScheme.onSurface.toArgb(),
+                windowingButtonColor = ColorStateList(
+                    arrayOf(
+                        intArrayOf(android.R.attr.state_pressed),
+                        intArrayOf(android.R.attr.state_focused),
+                        intArrayOf(android.R.attr.state_selected),
+                        intArrayOf(),
+                    ),
+                    intArrayOf(
+                        colorScheme.onSurface.toArgb(),
+                        colorScheme.onSurface.toArgb(),
+                        colorScheme.primary.toArgb(),
+                        colorScheme.onSurface.toArgb(),
+                    )
                 ),
-                intArrayOf(
-                    colorScheme.onSurface.toArgb(),
-                    colorScheme.onSurface.toArgb(),
-                    colorScheme.primary.toArgb(),
-                    colorScheme.onSurface.toArgb(),
+            )
+        }
+
+        private fun bindAppInfoPill(
+            style: MenuStyle,
+            appIconBitmap: Bitmap?,
+            appName: CharSequence?
+        ) {
+            appInfoPill.background.colorFilter = BlendModeColorFilter(
+                style.backgroundColor, BlendMode.MULTIPLY
+            )
+
+            collapseMenuButton.apply {
+                imageTintList = ColorStateList.valueOf(style.textColor)
+                this.taskInfo = this@HandleMenuView.taskInfo
+            }
+            appIconView.setImageBitmap(appIconBitmap)
+            appNameView.apply {
+                text = appName
+                setTextColor(style.textColor)
+            }
+        }
+
+        private fun bindWindowingPill(style: MenuStyle) {
+            windowingPill.background.colorFilter = BlendModeColorFilter(
+                style.backgroundColor, BlendMode.MULTIPLY
+            )
+
+            // TODO: Remove once implemented.
+            floatingBtn.visibility = View.GONE
+
+            fullscreenBtn.isSelected = taskInfo.isFullscreen
+            fullscreenBtn.imageTintList = style.windowingButtonColor
+            splitscreenBtn.isSelected = taskInfo.isMultiWindow
+            splitscreenBtn.imageTintList = style.windowingButtonColor
+            floatingBtn.isSelected = taskInfo.isPinned
+            floatingBtn.imageTintList = style.windowingButtonColor
+            desktopBtn.isSelected = taskInfo.isFreeform
+            desktopBtn.imageTintList = style.windowingButtonColor
+        }
+
+        private fun bindMoreActionsPill(style: MenuStyle) {
+            moreActionsPill.apply {
+                isGone = !shouldShowNewWindowButton && !SHOULD_SHOW_SCREENSHOT_BUTTON
+            }
+            screenshotBtn.apply {
+                isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON
+                background.colorFilter =
+                    BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY
                 )
-            ),
+                setTextColor(style.textColor)
+                compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+            }
+            newWindowBtn.apply {
+                isGone = !shouldShowNewWindowButton
+                background.colorFilter =
+                    BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+                setTextColor(style.textColor)
+                compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+            }
+        }
+
+        private fun bindOpenInBrowserPill(style: MenuStyle) {
+            openInBrowserPill.apply {
+                isGone = !shouldShowBrowserPill
+                background.colorFilter = BlendModeColorFilter(
+                    style.backgroundColor, BlendMode.MULTIPLY
+                )
+            }
+
+            browserBtn.apply {
+                setTextColor(style.textColor)
+                compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+            }
+        }
+
+        private data class MenuStyle(
+            @ColorInt val backgroundColor: Int,
+            @ColorInt val textColor: Int,
+            val windowingButtonColor: ColorStateList,
         )
     }
 
-    private data class MenuStyle(
-        @ColorInt val backgroundColor: Int,
-        @ColorInt val textColor: Int,
-        val windowingButtonColor: ColorStateList,
-    )
-
     companion object {
         private const val TAG = "HandleMenu"
-        private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false
+        private const val SHOULD_SHOW_SCREENSHOT_BUTTON = false
     }
 }
 
@@ -510,13 +635,11 @@
     fun create(
         parentDecor: DesktopModeWindowDecoration,
         layoutResId: Int,
-        onClickListener: View.OnClickListener?,
-        onTouchListener: View.OnTouchListener?,
         appIconBitmap: Bitmap?,
         appName: CharSequence?,
-        displayController: DisplayController,
         splitScreenController: SplitScreenController,
         shouldShowWindowingPill: Boolean,
+        shouldShowNewWindowButton: Boolean,
         openInBrowserLink: Uri?,
         captionWidth: Int,
         captionHeight: Int,
@@ -529,13 +652,11 @@
     override fun create(
         parentDecor: DesktopModeWindowDecoration,
         layoutResId: Int,
-        onClickListener: View.OnClickListener?,
-        onTouchListener: View.OnTouchListener?,
         appIconBitmap: Bitmap?,
         appName: CharSequence?,
-        displayController: DisplayController,
         splitScreenController: SplitScreenController,
         shouldShowWindowingPill: Boolean,
+        shouldShowNewWindowButton: Boolean,
         openInBrowserLink: Uri?,
         captionWidth: Int,
         captionHeight: Int,
@@ -544,13 +665,11 @@
         return HandleMenu(
             parentDecor,
             layoutResId,
-            onClickListener,
-            onTouchListener,
             appIconBitmap,
             appName,
-            displayController,
             splitScreenController,
             shouldShowWindowingPill,
+            shouldShowNewWindowButton,
             openInBrowserLink,
             captionWidth,
             captionHeight,
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 aa2ce0f..013f506 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
@@ -66,7 +66,6 @@
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
 import com.android.wm.shell.windowdecor.common.OPACITY_12
 import com.android.wm.shell.windowdecor.common.OPACITY_40
-import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
 import com.android.wm.shell.windowdecor.common.withAlpha
 import java.util.function.Supplier
 
@@ -102,15 +101,15 @@
 
     /** Creates and shows the maximize window. */
     fun show(
-        onMaximizeClickListener: OnTaskActionClickListener,
-        onLeftSnapClickListener: OnTaskActionClickListener,
-        onRightSnapClickListener: OnTaskActionClickListener,
+        onMaximizeOrRestoreClickListener: () -> Unit,
+        onLeftSnapClickListener: () -> Unit,
+        onRightSnapClickListener: () -> Unit,
         onHoverListener: (Boolean) -> Unit,
         onOutsideTouchListener: () -> Unit,
     ) {
         if (maximizeMenu != null) return
         createMaximizeMenu(
-            onMaximizeClickListener = onMaximizeClickListener,
+            onMaximizeClickListener = onMaximizeOrRestoreClickListener,
             onLeftSnapClickListener = onLeftSnapClickListener,
             onRightSnapClickListener = onRightSnapClickListener,
             onHoverListener = onHoverListener,
@@ -129,9 +128,9 @@
 
     /** Create a maximize menu that is attached to the display area. */
     private fun createMaximizeMenu(
-        onMaximizeClickListener: OnTaskActionClickListener,
-        onLeftSnapClickListener: OnTaskActionClickListener,
-        onRightSnapClickListener: OnTaskActionClickListener,
+        onMaximizeClickListener: () -> Unit,
+        onLeftSnapClickListener: () -> Unit,
+        onRightSnapClickListener: () -> Unit,
         onHoverListener: (Boolean) -> Unit,
         onOutsideTouchListener: () -> Unit
     ) {
@@ -165,17 +164,10 @@
             menuHeight = menuHeight,
             menuPadding = menuPadding,
         ).also { menuView ->
-            val taskId = taskInfo.taskId
             menuView.bind(taskInfo)
-            menuView.onMaximizeClickListener = {
-                onMaximizeClickListener.onClick(taskId, "maximize_menu_option")
-            }
-            menuView.onLeftSnapClickListener = {
-                onLeftSnapClickListener.onClick(taskId, "left_snap_option")
-            }
-            menuView.onRightSnapClickListener = {
-                onRightSnapClickListener.onClick(taskId, "right_snap_option")
-            }
+            menuView.onMaximizeClickListener = onMaximizeClickListener
+            menuView.onLeftSnapClickListener = onLeftSnapClickListener
+            menuView.onRightSnapClickListener = onRightSnapClickListener
             menuView.onMenuHoverListener = onHoverListener
             menuView.onOutsideTouchListener = onOutsideTouchListener
             viewHost.setView(menuView.rootView, lp)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index b5b476d..237492e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -163,14 +163,7 @@
         } else {
             DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
                     mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
-            if (!mTaskBoundsAtDragStart.equals(mRepositionTaskBounds)) {
-                final WindowContainerTransaction wct = new WindowContainerTransaction();
-                wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
-                mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
-            } else {
-                // Drag-move ended where it originally started, no need to update WM.
                 mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW);
-            }
         }
 
         mCtrlType = CTRL_TYPE_UNDEFINED;
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 6828560..4cab6e4 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
@@ -613,7 +613,7 @@
      * Create a window associated with this WindowDecoration.
      * Note that subclass must dispose of this when the task is hidden/closed.
      *
-     * @param layoutId     layout to make the window from
+     * @param v            View to attach to the window
      * @param t            the transaction to apply
      * @param xPos         x position of new window
      * @param yPos         y position of new window
@@ -621,9 +621,9 @@
      * @param height       height of new window
      * @return the {@link AdditionalViewHostViewContainer} that was added.
      */
-    AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix,
-            SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos,
-            int width, int height) {
+    AdditionalViewHostViewContainer addWindow(@NonNull View v, @NonNull String namePrefix,
+            @NonNull SurfaceControl.Transaction t, @NonNull SurfaceSyncGroup ssg,
+            int xPos, int yPos, int width, int height) {
         final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
         SurfaceControl windowSurfaceControl = builder
                 .setName(namePrefix + " of Task=" + mTaskInfo.taskId)
@@ -631,8 +631,6 @@
                 .setParent(mDecorationContainerSurface)
                 .setCallsite("WindowDecoration.addWindow")
                 .build();
-        View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null);
-
         t.setPosition(windowSurfaceControl, xPos, yPos)
                 .setWindowCrop(windowSurfaceControl, width, height)
                 .show(windowSurfaceControl);
@@ -653,6 +651,25 @@
     }
 
     /**
+     * Create a window associated with this WindowDecoration.
+     * Note that subclass must dispose of this when the task is hidden/closed.
+     *
+     * @param layoutId     layout to make the window from
+     * @param t            the transaction to apply
+     * @param xPos         x position of new window
+     * @param yPos         y position of new window
+     * @param width        width of new window
+     * @param height       height of new window
+     * @return the {@link AdditionalViewHostViewContainer} that was added.
+     */
+    AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix,
+            SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos,
+            int width, int height) {
+        final View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null);
+        return addWindow(v, namePrefix, t, ssg, xPos, yPos, width, height);
+    }
+
+    /**
      * Adds caption inset source to a WCT
      */
     public void addCaptionInset(WindowContainerTransaction wct) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index f1370bb..cadd80e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.windowdecor.additionalviewcontainer
 
+import android.annotation.LayoutRes
 import android.content.Context
 import android.graphics.PixelFormat
 import android.view.Gravity
@@ -29,24 +30,52 @@
  * for view containers that should be above the status bar layer.
  */
 class AdditionalSystemViewContainer(
-    private val context: Context,
+    context: Context,
     taskId: Int,
     x: Int,
     y: Int,
     width: Int,
     height: Int,
     flags: Int,
-    layoutId: Int? = null
-) : AdditionalViewContainer() {
     override val view: View
+) : AdditionalViewContainer() {
+
+    constructor(
+        context: Context,
+        taskId: Int,
+        x: Int,
+        y: Int,
+        width: Int,
+        height: Int,
+        flags: Int,
+        @LayoutRes layoutId: Int
+    ) : this(
+        context = context,
+        taskId = taskId,
+        x = x,
+        y = y,
+        width = width,
+        height = height,
+        flags = flags,
+        view = LayoutInflater.from(context).inflate(layoutId, null /* parent */)
+    )
+
+    constructor(
+        context: Context, taskId: Int, x: Int, y: Int, width: Int, height: Int, flags: Int
+    ) : this(
+        context = context,
+        taskId = taskId,
+        x = x,
+        y = y,
+        width = width,
+        height = height,
+        flags = flags,
+        view = View(context)
+    )
+
     val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java)
 
     init {
-        if (layoutId != null) {
-            view = LayoutInflater.from(context).inflate(layoutId, null)
-        } else {
-            view = View(context)
-        }
         val lp = WindowManager.LayoutParams(
             width, height, x, y,
             WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 57e469d..56fad95 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -68,6 +68,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.sysui.ShellSharedConstants;
+import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -114,6 +115,8 @@
     @Mock
     private ShellCommandHandler mShellCommandHandler;
     @Mock
+    private Transitions mTransitions;
+    @Mock
     private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
 
     private BackAnimationController mController;
@@ -156,7 +159,8 @@
                         mContentResolver,
                         mAnimationBackground,
                         mShellBackAnimationRegistry,
-                        mShellCommandHandler);
+                        mShellCommandHandler,
+                        mTransitions);
         mShellInit.init();
         mShellExecutor.flushAll();
         mTouchableRegion = new Rect(0, 0, 100, 100);
@@ -316,7 +320,8 @@
                         mContentResolver,
                         mAnimationBackground,
                         mShellBackAnimationRegistry,
-                        mShellCommandHandler);
+                        mShellCommandHandler,
+                        mTransitions);
         shellInit.init();
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
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 c3d31ba..e6c72cd 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
@@ -85,6 +85,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN
 import com.android.wm.shell.common.split.SplitScreenConstants
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask
@@ -128,10 +129,10 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
+import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 
 /**
@@ -1566,6 +1567,21 @@
   }
 
   @Test
+  fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() {
+    // Set up a visible freeform task
+    val freeformTask = setUpFreeformTask()
+    markTaskVisible(freeformTask)
+
+    // Mark recents animation running
+    recentsTransitionStateListener.onAnimationStateChanged(true)
+
+    // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
+    val result = controller.handleRequest(Binder(), createTransition(freeformTask))
+    assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode)
+      .isEqualTo(WINDOWING_MODE_UNDEFINED)
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
   fun handleRequest_topActivityTransparentWithStyleFloating_returnSwitchToFreeformWCT() {
     val freeformTask = setUpFreeformTask()
@@ -2361,7 +2377,7 @@
         task,
         Point(100, -100), /* position */
         PointF(200f, -200f), /* inputCoordinate */
-        Rect(100, -100, 500, 1000), /* taskBounds */
+        Rect(100, -100, 500, 1000), /* currentDragBounds */
         Rect(0, 50, 2000, 2000) /* validDragArea */)
     val rectAfterEnd = Rect(100, 50, 500, 1150)
     verify(transitions)
@@ -2376,6 +2392,40 @@
   }
 
   @Test
+  fun onDesktopDragEnd_noIndicator_updatesTaskBounds() {
+    val task = setUpFreeformTask()
+    val spyController = spy(controller)
+    val mockSurface = mock(SurfaceControl::class.java)
+    val mockDisplayLayout = mock(DisplayLayout::class.java)
+    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+
+    val currentDragBounds = Rect(100, 200, 500, 1000)
+    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+      .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+
+    spyController.onDragPositioningEnd(
+      task,
+      Point(100, 200), /* position */
+      PointF(200f, 300f), /* inputCoordinate */
+      currentDragBounds, /* currentDragBounds */
+      Rect(0, 50, 2000, 2000) /* validDragArea */)
+
+
+    verify(transitions)
+      .startTransition(
+        eq(TRANSIT_CHANGE),
+        Mockito.argThat { wct ->
+          return@argThat wct.changes.any { (token, change) ->
+            change.configuration.windowConfiguration.bounds == currentDragBounds
+          }
+        },
+        eq(null))
+  }
+
+  @Test
   fun enterSplit_freeformTaskIsMovedToSplit() {
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
@@ -2460,6 +2510,28 @@
   }
 
   @Test
+  fun getSnapBounds_calculatesBoundsForResizable() {
+    val bounds = Rect(100, 100, 300, 300)
+    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
+      topActivityInfo = ActivityInfo().apply {
+        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
+        configuration.windowConfiguration.appBounds = bounds
+      }
+      isResizeable = true
+    }
+
+    val currentDragBounds = Rect(0, 100, 200, 300)
+    val expectedBounds = Rect(
+      STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
+    )
+
+    controller.snapToHalfScreen(task, currentDragBounds, SnapPosition.LEFT)
+    // Assert bounds set to stable bounds
+    val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
+    assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+  }
+
+  @Test
   fun toggleBounds_togglesToCalculatedBoundsForNonResizable() {
     val bounds = Rect(0, 0, 200, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
@@ -2705,11 +2777,14 @@
     return arg.value
   }
 
-  private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction {
+  private fun getLatestToggleResizeDesktopTaskWct(
+    currentBounds: Rect? = null
+  ): WindowContainerTransaction {
     val arg: ArgumentCaptor<WindowContainerTransaction> =
         ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
     if (ENABLE_SHELL_TRANSITIONS) {
-      verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()).startTransition(capture(arg))
+      verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+        .startTransition(capture(arg), eq(currentBounds))
     } else {
       verify(shellTaskOrganizer).applyTransaction(capture(arg))
     }
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 e264160..bbf42b5 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
@@ -16,19 +16,23 @@
 package com.android.wm.shell.windowdecor
 
 import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.app.WindowConfiguration.WindowingMode
 import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
 import android.content.pm.ActivityInfo
 import android.graphics.Rect
 import android.hardware.display.DisplayManager
 import android.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
+import android.net.Uri
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
@@ -37,6 +41,7 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
 import android.testing.TestableLooper.RunWithLooper
 import android.util.SparseArray
 import android.view.Choreographer
@@ -47,11 +52,13 @@
 import android.view.InsetsSource
 import android.view.InsetsState
 import android.view.KeyEvent
+import android.view.Surface
 import android.view.SurfaceControl
 import android.view.SurfaceView
 import android.view.View
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
+import android.window.WindowContainerTransaction
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
@@ -65,11 +72,14 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
+import com.android.wm.shell.common.DisplayChangeController
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
@@ -81,13 +91,17 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
-import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
+import java.util.Optional
+import java.util.function.Consumer
+import java.util.function.Supplier
+import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentCaptor.forClass
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.anyInt
@@ -96,13 +110,14 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
-import java.util.Optional
-import java.util.function.Supplier
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 
@@ -145,22 +160,36 @@
     @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser
     private val bgExecutor = TestShellExecutor()
+    @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper
+    private lateinit var spyContext: TestableContext
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
         SurfaceControl.Transaction()
     }
     private val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>())
 
+    private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var shellInit: ShellInit
     private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+    private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener
     private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
 
     @Before
     fun setUp() {
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .spyStatic(DragPositioningCallbackUtility::class.java)
+                .startMocking()
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) }
+
+        spyContext = spy(mContext)
+        doNothing().`when`(spyContext).startActivity(any())
         shellInit = ShellInit(mockShellExecutor)
         windowDecorByTaskIdSpy.clear()
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
-                mContext,
+                spyContext,
                 mockShellExecutor,
                 mockMainHandler,
                 mockMainChoreographer,
@@ -176,6 +205,7 @@
                 mockTransitions,
                 Optional.of(mockDesktopTasksController),
                 mockGenericLinksParser,
+                mockMultiInstanceHelper,
                 mockDesktopModeWindowDecorFactory,
                 mockInputMonitorFactory,
                 transactionFactory,
@@ -195,10 +225,22 @@
 
         shellInit.init()
 
-        val listenerCaptor =
-                argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
-        verify(displayInsetsController).addInsetsChangedListener(anyInt(), listenerCaptor.capture())
-        desktopModeOnInsetsChangedListener = listenerCaptor.firstValue
+        val insetListenerCaptor =
+            argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
+        verify(displayInsetsController)
+            .addInsetsChangedListener(anyInt(), insetListenerCaptor.capture())
+        desktopModeOnInsetsChangedListener = insetListenerCaptor.firstValue
+
+        val displayChangingListenerCaptor =
+            argumentCaptor<DisplayChangeController.OnDisplayChangingListener>()
+        verify(mockDisplayController)
+            .addDisplayChangingController(displayChangingListenerCaptor.capture())
+        displayChangingListener = displayChangingListenerCaptor.firstValue
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
     }
 
     @Test
@@ -296,7 +338,7 @@
         whenever(view.id).thenReturn(R.id.back_button)
 
         val inputManager = mock(InputManager::class.java)
-        mContext.addMockSystemService(InputManager::class.java, inputManager)
+        spyContext.addMockSystemService(InputManager::class.java, inputManager)
 
         val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java)
         desktopModeWindowDecorViewModel
@@ -334,24 +376,16 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
     fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() {
-        val mockitoSession: StaticMockitoSession = mockitoSession()
-                .strictness(Strictness.LENIENT)
-                .spyStatic(DesktopModeStatus::class.java)
-                .startMocking()
-        try {
-            val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
-                isTopActivityTransparent = true
-                isTopActivityStyleFloating = true
-                numActivities = 1
-            }
-            doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
-            setUpMockDecorationsForTasks(task)
-
-            onTaskOpening(task)
-            assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
-        } finally {
-            mockitoSession.finishMocking()
+        val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+            isTopActivityTransparent = true
+            isTopActivityStyleFloating = true
+            numActivities = 1
         }
+        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
+        setUpMockDecorationsForTasks(task)
+
+        onTaskOpening(task)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
     }
 
     @Test
@@ -463,96 +497,70 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() {
-        val mockitoSession: StaticMockitoSession = mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .spyStatic(DesktopModeStatus::class.java)
-            .startMocking()
-        try {
-            // Simulate default enforce device restrictions system property
-            whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
+        // Simulate default enforce device restrictions system property
+        whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
 
-            val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
-            // Simulate device that doesn't support desktop mode
-            doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+        // Simulate device that doesn't support desktop mode
+        doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
-            onTaskOpening(task)
-            assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
-        } finally {
-            mockitoSession.finishMocking()
-        }
+        onTaskOpening(task)
+        assertFalse(windowDecorByTaskIdSpy.contains(task.taskId))
     }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() {
-        val mockitoSession: StaticMockitoSession = mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .spyStatic(DesktopModeStatus::class.java)
-            .startMocking()
-        try {
-            // Simulate enforce device restrictions system property overridden to false
-            whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
-            // Simulate device that doesn't support desktop mode
-            doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        // Simulate enforce device restrictions system property overridden to false
+        whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
+        // Simulate device that doesn't support desktop mode
+        doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
-            val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
-            setUpMockDecorationsForTasks(task)
+        val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+        setUpMockDecorationsForTasks(task)
 
-            onTaskOpening(task)
-            assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
-        } finally {
-            mockitoSession.finishMocking()
-        }
+        onTaskOpening(task)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
     }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() {
-        val mockitoSession: StaticMockitoSession = mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .spyStatic(DesktopModeStatus::class.java)
-            .startMocking()
-        try {
-            // Simulate default enforce device restrictions system property
-            whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
+        // Simulate default enforce device restrictions system property
+        whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
 
-            val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
-            doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-            setUpMockDecorationsForTasks(task)
+        val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        setUpMockDecorationsForTasks(task)
 
-            onTaskOpening(task)
-            assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
-        } finally {
-            mockitoSession.finishMocking()
-        }
+        onTaskOpening(task)
+        assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
     }
 
     @Test
     fun testOnDecorMaximizedOrRestored_togglesTaskSize() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture())
-                return@let captor.value
-            }
+        val maxOrRestoreListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor
+        )
 
-        maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test")
+        maxOrRestoreListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo)
     }
 
     @Test
     fun testOnDecorMaximizedOrRestored_closesMenus() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture())
-                return@let captor.value
-            }
+        val maxOrRestoreListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor
+        )
 
-        maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test")
+        maxOrRestoreListenerCaptor.value.invoke()
 
         verify(decor).closeHandleMenu()
         verify(decor).closeMaximizeMenu()
@@ -560,30 +568,30 @@
 
     @Test
     fun testOnDecorSnappedLeft_snapResizes() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnLeftSnapClickListener(captor.capture())
-                return@let captor.value
-            }
+        val onLeftSnapClickListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor
+        )
 
-        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+        val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds
+        onLeftSnapClickListenerCaptor.value.invoke()
 
-        verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.LEFT)
+        verify(mockDesktopTasksController)
+            .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.LEFT)
     }
 
     @Test
     fun testOnDecorSnappedLeft_closeMenus() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnLeftSnapClickListener(captor.capture())
-                return@let captor.value
-            }
+        val onLeftSnapClickListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor
+        )
 
-        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+        onLeftSnapClickListenerCaptor.value.invoke()
 
         verify(decor).closeHandleMenu()
         verify(decor).closeMaximizeMenu()
@@ -591,35 +599,365 @@
 
     @Test
     fun testOnDecorSnappedRight_snapResizes() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnRightSnapClickListener(captor.capture())
-                return@let captor.value
-            }
+        val onRightSnapClickListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor
+        )
 
-        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+        val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds
+        onRightSnapClickListenerCaptor.value.invoke()
 
-        verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.RIGHT)
+        verify(mockDesktopTasksController)
+            .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.RIGHT)
     }
 
     @Test
     fun testOnDecorSnappedRight_closeMenus() {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
-        onTaskOpening(decor.mTaskInfo)
-        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
-            .let { captor ->
-                verify(decor).setOnRightSnapClickListener(captor.capture())
-                return@let captor.value
-            }
+        val onRightSnapClickListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor
+        )
 
-        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+        onRightSnapClickListenerCaptor.value.invoke()
 
         verify(decor).closeHandleMenu()
         verify(decor).closeMaximizeMenu()
     }
 
+    @Test
+    fun testDecor_onClickToDesktop_movesToDesktopWithSource() {
+        val toDesktopListenerCaptor = forClass(Consumer::class.java)
+                as ArgumentCaptor<Consumer<DesktopModeTransitionSource>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FULLSCREEN,
+            onToDesktopClickListenerCaptor = toDesktopListenerCaptor
+        )
+
+        toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
+
+        verify(mockDesktopTasksController).moveTaskToDesktop(
+            eq(decor.mTaskInfo.taskId),
+            any(),
+            eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
+        )
+    }
+
+    @Test
+    fun testDecor_onClickToDesktop_addsCaptionInsets() {
+        val toDesktopListenerCaptor = forClass(Consumer::class.java)
+                as ArgumentCaptor<Consumer<DesktopModeTransitionSource>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FULLSCREEN,
+            onToDesktopClickListenerCaptor = toDesktopListenerCaptor
+        )
+
+        toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
+
+        verify(decor).addCaptionInset(any())
+    }
+
+    @Test
+    fun testDecor_onClickToDesktop_closesHandleMenu() {
+        val toDesktopListenerCaptor = forClass(Consumer::class.java)
+                    as ArgumentCaptor<Consumer<DesktopModeTransitionSource>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FULLSCREEN,
+            onToDesktopClickListenerCaptor = toDesktopListenerCaptor
+        )
+
+        toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
+
+        verify(decor).closeHandleMenu()
+    }
+
+    @Test
+    fun testDecor_onClickToFullscreen_closesHandleMenu() {
+        val toFullscreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor
+        )
+
+        toFullscreenListenerCaptor.value.invoke()
+
+        verify(decor).closeHandleMenu()
+    }
+
+    @Test
+    fun testDecor_onClickToFullscreen_isFreeform_movesToFullscreen() {
+        val toFullscreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor
+        )
+
+        toFullscreenListenerCaptor.value.invoke()
+
+        verify(mockDesktopTasksController).moveToFullscreen(
+            decor.mTaskInfo.taskId,
+            DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON
+        )
+    }
+
+    @Test
+    fun testDecor_onClickToFullscreen_isSplit_movesToFullscreen() {
+        val toFullscreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_MULTI_WINDOW,
+            onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor
+        )
+
+        toFullscreenListenerCaptor.value.invoke()
+
+        verify(mockSplitScreenController).moveTaskToFullscreen(
+            decor.mTaskInfo.taskId,
+            SplitScreenController.EXIT_REASON_DESKTOP_MODE
+        )
+    }
+
+    @Test
+    fun testDecor_onClickToSplitScreen_closesHandleMenu() {
+        val toSplitScreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_MULTI_WINDOW,
+            onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor
+        )
+
+        toSplitScreenListenerCaptor.value.invoke()
+
+        verify(decor).closeHandleMenu()
+    }
+
+    @Test
+    fun testDecor_onClickToSplitScreen_requestsSplit() {
+        val toSplitScreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_MULTI_WINDOW,
+            onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor
+        )
+
+        toSplitScreenListenerCaptor.value.invoke()
+
+        verify(mockDesktopTasksController).requestSplit(decor.mTaskInfo, leftOrTop = false)
+    }
+
+    @Test
+    fun testDecor_onClickToSplitScreen_disposesStatusBarInputLayer() {
+        val toSplitScreenListenerCaptor = forClass(Function0::class.java)
+                as ArgumentCaptor<Function0<Unit>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_MULTI_WINDOW,
+            onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor
+        )
+
+        toSplitScreenListenerCaptor.value.invoke()
+
+        verify(decor).disposeStatusBarInputLayer()
+    }
+
+    @Test
+    fun testDecor_onClickToOpenBrowser_closeMenus() {
+        val openInBrowserListenerCaptor = forClass(Consumer::class.java)
+                as ArgumentCaptor<Consumer<Uri>>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FULLSCREEN,
+            onOpenInBrowserClickListener = openInBrowserListenerCaptor
+        )
+
+        openInBrowserListenerCaptor.value.accept(Uri.EMPTY)
+
+        verify(decor).closeHandleMenu()
+        verify(decor).closeMaximizeMenu()
+    }
+
+    @Test
+    fun testDecor_onClickToOpenBrowser_opensBrowser() {
+        doNothing().whenever(spyContext).startActivity(any())
+        val uri = Uri.parse("https://www.google.com")
+        val openInBrowserListenerCaptor = forClass(Consumer::class.java)
+                as ArgumentCaptor<Consumer<Uri>>
+        createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FULLSCREEN,
+            onOpenInBrowserClickListener = openInBrowserListenerCaptor
+        )
+
+        openInBrowserListenerCaptor.value.accept(uri)
+
+        verify(spyContext).startActivity(argThat { intent ->
+            intent.data == uri
+                    && ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0)
+                    && intent.categories.contains(Intent.CATEGORY_LAUNCHER)
+                    && intent.action == Intent.ACTION_MAIN
+        })
+    }
+
+    @Test
+    fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct).setBounds(eq(secondTask.token), any())
+        verify(wct).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(false).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct, never()).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_180, null, wct
+        )
+
+        verify(wct, never()).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN)
+        val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    private fun createOpenTaskDecoration(
+        @WindowingMode windowingMode: Int,
+        onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
+            forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
+        onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
+            forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
+        onRightSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
+            forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
+        onToDesktopClickListenerCaptor: ArgumentCaptor<Consumer<DesktopModeTransitionSource>> =
+            forClass(Consumer::class.java) as ArgumentCaptor<Consumer<DesktopModeTransitionSource>>,
+        onToFullscreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
+            forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
+        onToSplitScreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
+            forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
+        onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Uri>> =
+            forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Uri>>,
+    ): DesktopModeWindowDecoration {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode))
+        onTaskOpening(decor.mTaskInfo)
+        verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture())
+        verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture())
+        verify(decor).setOnRightSnapClickListener(onRightSnapClickListenerCaptor.capture())
+        verify(decor).setOnToDesktopClickListener(onToDesktopClickListenerCaptor.capture())
+        verify(decor).setOnToFullscreenClickListener(onToFullscreenClickListenerCaptor.capture())
+        verify(decor).setOnToSplitScreenClickListener(onToSplitScreenClickListenerCaptor.capture())
+        verify(decor).setOpenInBrowserClickListener(onOpenInBrowserClickListener.capture())
+        return decor
+    }
+
     private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
         desktopModeWindowDecorViewModel.onTaskOpening(
                 task,
@@ -640,7 +978,7 @@
 
     private fun createTask(
             displayId: Int = DEFAULT_DISPLAY,
-            @WindowConfiguration.WindowingMode windowingMode: Int,
+            @WindowingMode windowingMode: Int,
             activityType: Int = ACTIVITY_TYPE_STANDARD,
             focused: Boolean = true,
             activityInfo: ActivityInfo = ActivityInfo(),
@@ -659,11 +997,17 @@
     private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
         val decoration = mock(DesktopModeWindowDecoration::class.java)
         whenever(
-            mockDesktopModeWindowDecorFactory.create(any(), any(), any(), any(), any(), eq(task),
-                any(), any(), any(), any(), any(), any(), any())
+            mockDesktopModeWindowDecorFactory.create(
+                any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.isFocused).thenReturn(task.isFocused)
+        if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+            whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
+                .thenReturn(true)
+        }
+        whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600))
         return decoration
     }
 
@@ -684,7 +1028,7 @@
         )
     }
 
-    private fun RunningTaskInfo.setWindowingMode(@WindowConfiguration.WindowingMode mode: Int) {
+    private fun RunningTaskInfo.setWindowingMode(@WindowingMode mode: Int) {
         configuration.windowConfiguration.windowingMode = mode
     }
 
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 04b1eed..596adfb 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
@@ -89,14 +89,15 @@
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.MultiInstanceHelper;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
-import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 
 import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
 import kotlin.jvm.functions.Function1;
 
 import org.junit.After;
@@ -110,6 +111,7 @@
 import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -167,13 +169,14 @@
     @Mock
     private Handler mMockHandler;
     @Mock
-    private DesktopModeWindowDecoration.OpenInBrowserClickListener mMockOpenInBrowserClickListener;
+    private Consumer<Uri> mMockOpenInBrowserClickListener;
     @Mock
     private AppToWebGenericLinksParser mMockGenericLinksParser;
     @Mock
     private HandleMenu mMockHandleMenu;
     @Mock
     private HandleMenuFactory mMockHandleMenuFactory;
+    private MultiInstanceHelper mMockMultiInstanceHelper;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -217,8 +220,9 @@
         final Display defaultDisplay = mock(Display.class);
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
-        doReturn(mMockHandleMenu).when(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(),
-                any(), any(), any(), any(), anyBoolean(), any(), anyInt(), anyInt(), anyInt());
+        when(mMockHandleMenuFactory.create(any(), anyInt(), any(), any(),
+                any(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt()))
+                .thenReturn(mMockHandleMenu);
     }
 
     @After
@@ -739,9 +743,21 @@
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
                 taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+        final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+                ArgumentCaptor.forClass(Function1.class);
+
         // Simulate menu opening and clicking open in browser button
         decor.createHandleMenu(mMockSplitScreenController);
-        decor.onOpenInBrowserClick();
+        verify(mMockHandleMenu).show(
+                any(),
+                any(),
+                any(),
+                any(),
+                openInBrowserCaptor.capture(),
+                any(),
+                any()
+        );
+        openInBrowserCaptor.getValue().invoke(TEST_URI1);
 
         // Verify handle menu's browser link not set to captured link since link not valid after
         // open in browser clicked
@@ -755,10 +771,22 @@
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
                 taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+        final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+                ArgumentCaptor.forClass(Function1.class);
         decor.createHandleMenu(mMockSplitScreenController);
-        decor.onOpenInBrowserClick();
+        verify(mMockHandleMenu).show(
+                any(),
+                any(),
+                any(),
+                any(),
+                openInBrowserCaptor.capture(),
+                any(),
+                any()
+        );
 
-        verify(mMockOpenInBrowserClickListener).onClick(any(), any());
+        openInBrowserCaptor.getValue().invoke(TEST_URI1);
+
+        verify(mMockOpenInBrowserClickListener).accept(TEST_URI1);
     }
 
     @Test
@@ -773,13 +801,37 @@
         verifyHandleMenuCreated(TEST_URI2);
     }
 
+    @Test
+    public void handleMenu_onCloseMenuClick_closesMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                true /* relayout */);
+        final ArgumentCaptor<Function0<Unit>> closeClickListener =
+                ArgumentCaptor.forClass(Function0.class);
+        decoration.createHandleMenu(mMockSplitScreenController);
+        verify(mMockHandleMenu).show(
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                closeClickListener.capture(),
+                any()
+        );
+
+        closeClickListener.getValue().invoke();
+
+        verify(mMockHandleMenu).close();
+        assertFalse(decoration.isHandleMenuActive());
+    }
+
     private void verifyHandleMenuCreated(@Nullable Uri uri) {
-        verify(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(), any(), any(), any(),
-                any(), anyBoolean(), eq(uri), anyInt(), anyInt(), anyInt());
+        verify(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(),
+                any(), anyBoolean(), anyBoolean(), eq(uri), anyInt(), anyInt(), anyInt());
     }
 
     private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
-        final OnTaskActionClickListener l = (taskId, tag) -> {};
+        final Function0<Unit> l = () -> Unit.INSTANCE;
         decoration.setOnMaximizeOrRestoreClickListener(l);
         decoration.setOnLeftSnapClickListener(l);
         decoration.setOnRightSnapClickListener(l);
@@ -811,32 +863,47 @@
         taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
         final String genericLinkString = genericLink == null ? null : genericLink.toString();
         doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
-        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
         // Relayout to set captured link
-        decor.relayout(taskInfo);
-        return decor;
+        return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(), true /* relayout */);
     }
 
     private DesktopModeWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo) {
-        return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory());
+        return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(),
+                false /* relayout */);
+    }
+
+    private DesktopModeWindowDecoration createWindowDecoration(
+            ActivityManager.RunningTaskInfo taskInfo, boolean relayout) {
+        return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(), relayout);
     }
 
     private DesktopModeWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo,
             MaximizeMenuFactory maximizeMenuFactory) {
+        return createWindowDecoration(taskInfo, maximizeMenuFactory, false /* relayout */);
+    }
+
+    private DesktopModeWindowDecoration createWindowDecoration(
+            ActivityManager.RunningTaskInfo taskInfo,
+            MaximizeMenuFactory maximizeMenuFactory,
+            boolean relayout) {
         final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
                 mContext, mMockDisplayController, mMockSplitScreenController,
                 mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
                 mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
                 mMockGenericLinksParser, SurfaceControl.Builder::new, mMockTransactionSupplier,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory);
+                mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory,
+                        mMockMultiInstanceHelper);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
         windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
         windowDecor.mDecorWindowContext = mContext;
+        if (relayout) {
+            windowDecor.relayout(taskInfo);
+        }
         return windowDecor;
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 6667504..2ce59ff 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -35,7 +35,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.any
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index ed43aa3..a1c7947 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -57,6 +57,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
 /**
@@ -109,6 +110,9 @@
         whenever(mockDesktopWindowDecoration.addWindow(
             anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt())
         ).thenReturn(mockAdditionalViewHostViewContainer)
+        whenever(mockDesktopWindowDecoration.addWindow(
+            any<View>(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt())
+        ).thenReturn(mockAdditionalViewHostViewContainer)
         whenever(mockAdditionalViewHostViewContainer.view).thenReturn(menuView)
         whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
         whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
@@ -226,13 +230,13 @@
             }
             else -> error("Invalid windowing mode")
         }
-        val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
-                onClickListener, onTouchListener, appIcon, appName, displayController,
-                splitScreenController, shouldShowWindowingPill = true,
-                null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
-                captionX = captionX
+        val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, appIcon, appName,
+            splitScreenController, shouldShowWindowingPill = true,
+            shouldShowNewWindowButton = true,
+            null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+            captionX = captionX
         )
-        handleMenu.show()
+        handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock())
         return handleMenu
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 943c313..08a6e1b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -200,7 +200,7 @@
         verify(mockTransaction).setPosition(any(), eq(rectAfterMove.left.toFloat()),
                 eq(rectAfterMove.top.toFloat()))
 
-        taskPositioner.onDragPositioningEnd(
+        val endBounds = taskPositioner.onDragPositioningEnd(
             STARTING_BOUNDS.left.toFloat() + 70,
             STARTING_BOUNDS.top.toFloat() + 20
         )
@@ -212,12 +212,7 @@
 
         verify(mockDesktopWindowDecoration, never()).showResizeVeil(any())
         verify(mockDesktopWindowDecoration, never()).hideResizeVeil()
-        verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
-            return@argThat wct.changes.any { (token, change) ->
-                token == taskBinder &&
-                        (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
-                        change.configuration.windowConfiguration.bounds == rectAfterEnd }},
-                eq(taskPositioner))
+        Assert.assertEquals(rectAfterEnd, endBounds)
     }
 
     @Test
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 97971e1..3196ba1 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -23,4 +23,12 @@
     namespace: "media_tv"
     description: "TIAF V3.0 APIs for Android V"
     bug: "303323657"
+}
+
+flag {
+    name: "tis_always_bound_permission"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Introduce ALWAYS_BOUND_TV_INPUT for TIS."
+    bug: "332201346"
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index a75aeaf..d6e19a6 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -104,7 +104,6 @@
         scrollable(Screen.MultipleCredentialsScreenFlatten.route) {
             MultiCredentialsFlattenScreen(
                 credentialSelectorUiState = (remember { uiState } as MultipleEntry),
-                columnState = it.columnState,
                 flowEngine = flowEngine,
             )
         }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
index 25bc381..e58de64 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
@@ -19,10 +19,11 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.material3.Icon
 import android.graphics.drawable.Drawable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Lock
 import androidx.compose.material.icons.outlined.LockOpen
@@ -62,7 +63,7 @@
                         WearButtonText(
                             text = label,
                             textAlign = textAlign,
-                            maxLines = if (secondaryLabel != null) 1 else 2
+                            maxLines = 2
                         )
                     },
                     secondaryLabel,
@@ -88,7 +89,13 @@
     ) {
     val labelParam: (@Composable RowScope.() -> Unit) =
         {
-            text()
+            var horizontalArrangement = Arrangement.Start
+            if (icon == null) {
+                horizontalArrangement = Arrangement.Center
+            }
+            Row(horizontalArrangement = horizontalArrangement, modifier = modifier.fillMaxWidth()) {
+                text()
+            }
         }
 
     val secondaryLabelParam: (@Composable RowScope.() -> Unit)? =
@@ -97,6 +104,7 @@
                 Row {
                     WearSecondaryLabel(
                         text = secondaryLabel,
+                        color = WearMaterialTheme.colors.onSurfaceVariant
                     )
 
                     if (isAuthenticationEntryLocked != null) {
@@ -178,6 +186,7 @@
             WearButtonText(
                 text = stringResource(R.string.dialog_continue_button),
                 textAlign = TextAlign.Center,
+                color = WearMaterialTheme.colors.surface,
             )
         },
         colors = ChipDefaults.primaryChipColors(),
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
index 0afef5e..a82360b 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
@@ -16,9 +16,11 @@
 
 package com.android.credentialmanager.ui.components
 
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import android.graphics.drawable.Drawable
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
@@ -36,26 +38,30 @@
     icon: Drawable?,
     title: String,
 ) {
-    Column(
-        modifier = Modifier,
-        horizontalAlignment = Alignment.CenterHorizontally
-    ) {
-        if (icon != null) {
-            Icon(
-                bitmap = icon.toBitmap().asImageBitmap(),
-                modifier = Modifier.size(24.dp),
-                // Decorative purpose only.
-                contentDescription = null,
-                tint = Color.Unspecified,
 
+    Row {
+        Spacer(Modifier.weight(0.073f)) // 7.3% side margin
+        Column(
+            modifier = Modifier.weight(0.854f).fillMaxSize(),
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            if (icon != null) {
+                Icon(
+                    bitmap = icon.toBitmap().asImageBitmap(),
+                    modifier = Modifier.size(24.dp),
+                    // Decorative purpose only.
+                    contentDescription = null,
+                    tint = Color.Unspecified,
+                    )
+            }
+            Spacer(modifier = Modifier.size(8.dp))
+
+            WearTitleText(
+                text = title,
             )
+
+            Spacer(modifier = Modifier.size(8.dp))
         }
-        Spacer(modifier = Modifier.size(8.dp))
-
-        WearTitleText(
-            text = title,
-        )
-
-        Spacer(modifier = Modifier.size(8.dp))
+        Spacer(Modifier.weight(0.073f)) // 7.3% side margin
     }
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
index 282fea0..a7b13ad 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
@@ -16,6 +16,7 @@
 
 package com.android.credentialmanager.common.ui.components
 
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material3.Text
@@ -43,7 +44,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = WearMaterialTheme.colors.onSurfaceVariant,
+        color = WearMaterialTheme.colors.onSurface,
         textAlign = TextAlign.Center,
         overflow = TextOverflow.Ellipsis,
         maxLines = 2,
@@ -60,7 +61,7 @@
     Text(
         modifier = modifier.padding(start = 8.dp, end = 8.dp).wrapContentSize(),
         text = text,
-        color = WearMaterialTheme.colors.onSurfaceVariant,
+        color = WearMaterialTheme.colors.onSurface,
         style = WearMaterialTheme.typography.caption1,
         overflow = TextOverflow.Ellipsis,
         textAlign = textAlign,
@@ -91,12 +92,13 @@
 @Composable
 fun WearSecondaryLabel(
     text: String,
-    modifier: Modifier = Modifier,
+    color: Color = WearMaterialTheme.colors.onSurface,
+    modifier: Modifier = Modifier
 ) {
     Text(
-        modifier = modifier.wrapContentSize(),
+        modifier = modifier.fillMaxSize(),
         text = text,
-        color = WearMaterialTheme.colors.onSurfaceVariant,
+        color = color,
         style = WearMaterialTheme.typography.caption1,
         overflow = TextOverflow.Ellipsis,
         textAlign = TextAlign.Start,
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
index 36e9792..2af5be8 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
@@ -15,14 +15,18 @@
  */
 package com.android.credentialmanager.ui.screens.multiple
 
-import com.android.credentialmanager.ui.components.CredentialsScreenChip
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import com.android.credentialmanager.ui.components.CredentialsScreenChip
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.Alignment
 import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
 import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.R
@@ -32,13 +36,13 @@
 import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumn
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.rememberColumnState
+import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
 
 /**
  * Screen that shows multiple credentials to select from, grouped by accounts
  *
  * @param credentialSelectorUiState The app bar view model.
- * @param columnState ScalingLazyColumn configuration to be be applied
  * @param modifier styling for composable
  * @param flowEngine [FlowEngine] that updates ui state for this screen
  */
@@ -46,28 +50,45 @@
 @Composable
 fun MultiCredentialsFlattenScreen(
     credentialSelectorUiState: MultipleEntry,
-    columnState: ScalingLazyColumnState,
     flowEngine: FlowEngine,
 ) {
     val selectEntry = flowEngine.getEntrySelector()
-    ScalingLazyColumn(
-        columnState = columnState,
-        modifier = Modifier.fillMaxSize(),
-    ) {
+    Row {
+        Spacer(Modifier.weight(0.052f)) // 5.2% side margin
+        ScalingLazyColumn(
+            columnState = rememberColumnState(
+                ScalingLazyColumnDefaults.belowTimeText(horizontalAlignment = Alignment.Start),
+            ),
+            modifier = Modifier.weight(0.896f).fillMaxSize(), // 5.2% side margin
+        ) {
+
         item {
-            // make this credential specific if all credentials are same
-            WearButtonText(
-                text = stringResource(R.string.sign_in_options_title),
-                textAlign = TextAlign.Start,
-            )
+            Row {
+                Spacer(Modifier.weight(0.073f)) // 7.3% side margin
+                WearButtonText(
+                    text = stringResource(R.string.sign_in_options_title),
+                    textAlign = TextAlign.Center,
+                    modifier = Modifier.weight(0.854f).fillMaxSize(),
+                )
+                Spacer(Modifier.weight(0.073f)) // 7.3% side margin
+            }
         }
 
         credentialSelectorUiState.accounts.forEach { userNameEntries ->
             item {
-                WearSecondaryLabel(
-                    text = userNameEntries.userName,
-                    modifier = Modifier.padding(top = 12.dp, bottom = 4.dp)
-                )
+                Row {
+                    Spacer(Modifier.weight(0.0624f)) // 6.24% side margin
+                    WearSecondaryLabel(
+                        text = userNameEntries.userName,
+                        modifier = Modifier.padding(
+                            top = 12.dp,
+                            bottom = 4.dp,
+                            start = 0.dp,
+                            end = 0.dp
+                        ).fillMaxWidth(0.87f)
+                    )
+                    Spacer(Modifier.weight(0.0624f)) // 6.24% side margin
+                }
             }
 
             userNameEntries.sortedCredentialEntryList.forEach { credential: CredentialEntryInfo ->
@@ -78,7 +99,7 @@
                         secondaryLabel =
                         credential.credentialTypeDisplayName.ifEmpty {
                             credential.providerDisplayName
-                         },
+                        },
                         icon = credential.icon,
                         textAlign = TextAlign.Start
                     )
@@ -87,14 +108,25 @@
                 }
             }
         }
-        item {
-            WearSecondaryLabel(
-                text = stringResource(R.string.provider_list_title),
-                modifier = Modifier.padding(top = 12.dp, bottom = 4.dp)
-            )
-        }
-        credentialSelectorUiState.actionEntryList.forEach { actionEntry ->
+
+        if (credentialSelectorUiState.actionEntryList.isNotEmpty()) {
             item {
+                Row {
+                    Spacer(Modifier.weight(0.0624f)) // 6.24% side margin
+                    WearSecondaryLabel(
+                        text = stringResource(R.string.provider_list_title),
+                        modifier = Modifier.padding(
+                            top = 12.dp,
+                            bottom = 4.dp,
+                            start = 0.dp,
+                            end = 0.dp
+                        ).fillMaxWidth(0.87f)
+                )
+                    Spacer(Modifier.weight(0.0624f)) // 6.24% side margin
+                }
+            }
+            credentialSelectorUiState.actionEntryList.forEach { actionEntry ->
+                item {
                     CredentialsScreenChip(
                         label = actionEntry.title,
                         onClick = { selectEntry(actionEntry, false) },
@@ -102,7 +134,10 @@
                         icon = actionEntry.icon,
                     )
                     CredentialsScreenChipSpacer()
+                }
             }
         }
     }
+    Spacer(Modifier.weight(0.052f)) // 5.2% side margin
+    }
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
index ce2bad0..38307b0 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
@@ -16,10 +16,12 @@
 
 package com.android.credentialmanager.ui.screens.multiple
 
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.Row
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.foundation.layout.fillMaxSize
 import com.android.credentialmanager.R
 import androidx.compose.ui.res.stringResource
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -53,29 +55,31 @@
     flowEngine: FlowEngine,
 ) {
     val selectEntry = flowEngine.getEntrySelector()
-    ScalingLazyColumn(
-        columnState = columnState,
-        modifier = Modifier.fillMaxSize(),
-    ) {
-        // flatten all credentials into one
-        val credentials = credentialSelectorUiState.sortedEntries
-        item {
-            var title = stringResource(R.string.choose_sign_in_title)
-            if (credentials.isNotEmpty()) {
-                if (credentials.all { it.credentialType == CredentialType.PASSKEY }) {
-                    title = stringResource(R.string.choose_passkey_title)
-                } else if (credentials.all { it.credentialType == CredentialType.PASSWORD }) {
-                    title = stringResource(R.string.choose_password_title)
+    Row {
+        Spacer(Modifier.weight(0.052f)) // 5.2% side margin
+        ScalingLazyColumn(
+            columnState = columnState,
+            modifier = Modifier.weight(0.896f).fillMaxSize(),
+        ) {
+            // flatten all credentials into one
+            val credentials = credentialSelectorUiState.sortedEntries
+            item {
+                var title = stringResource(R.string.choose_sign_in_title)
+                if (credentials.isNotEmpty()) {
+                    if (credentials.all { it.credentialType == CredentialType.PASSKEY }) {
+                        title = stringResource(R.string.choose_passkey_title)
+                    } else if (credentials.all { it.credentialType == CredentialType.PASSWORD }) {
+                        title = stringResource(R.string.choose_password_title)
+                    }
                 }
+
+                SignInHeader(
+                    icon = credentialSelectorUiState.icon,
+                    title = title,
+                )
             }
 
-            SignInHeader(
-                icon = credentialSelectorUiState.icon,
-                title = title,
-            )
-        }
-
-        credentials.forEach { credential: CredentialEntryInfo ->
+            credentials.forEach { credential: CredentialEntryInfo ->
                 item {
                     CredentialsScreenChip(
                         label = credential.userName,
@@ -85,29 +89,32 @@
                             credential.providerDisplayName
                         },
                         icon = credential.icon,
+                        textAlign = TextAlign.Start
                     )
                     CredentialsScreenChipSpacer()
                 }
             }
 
-        credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo ->
-            item {
-                LockedProviderChip(authenticationEntryInfo) {
-                    selectEntry(authenticationEntryInfo, false)
+            credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo ->
+                item {
+                    LockedProviderChip(authenticationEntryInfo) {
+                        selectEntry(authenticationEntryInfo, false)
+                    }
+                    CredentialsScreenChipSpacer()
                 }
-                CredentialsScreenChipSpacer()
+            }
+            item {
+                Spacer(modifier = Modifier.size(8.dp))
+            }
+
+            item {
+                SignInOptionsChip { flowEngine.openSecondaryScreen() }
+            }
+            item {
+                DismissChip { flowEngine.cancel() }
+                BottomSpacer()
             }
         }
-        item {
-            Spacer(modifier = Modifier.size(8.dp))
+            Spacer(Modifier.weight(0.052f)) // 5.2% side margin
         }
-
-        item {
-            SignInOptionsChip { flowEngine.openSecondaryScreen() }
-        }
-        item {
-            DismissChip { flowEngine.cancel() }
-            BottomSpacer()
-        }
-    }
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
index e94dd68..17dd962 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
@@ -18,6 +18,8 @@
 
 package com.android.credentialmanager.ui.screens.single
 
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -31,15 +33,18 @@
     headerContent: @Composable () -> Unit,
     accountContent: @Composable () -> Unit,
     columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
     content: ScalingLazyListScope.() -> Unit,
 ) {
-    ScalingLazyColumn(
-        columnState = columnState,
-        modifier = modifier.fillMaxSize(),
-    ) {
-        item { headerContent() }
-        item { accountContent() }
-        content()
+    Row {
+        Spacer(Modifier.weight(0.052f)) // 5.2% side margin
+        ScalingLazyColumn(
+            columnState = columnState,
+            modifier = Modifier.weight(0.896f).fillMaxSize(),
+        ) {
+            item { headerContent() }
+            item { accountContent() }
+            content()
+        }
+        Spacer(Modifier.weight(0.052f)) // 5.2% side margin
     }
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index 03608a4..ce243b0 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -19,11 +19,8 @@
 package com.android.credentialmanager.ui.screens.single.passkey
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
 import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.R
@@ -75,7 +72,6 @@
             }
         },
         columnState = columnState,
-        modifier = Modifier.padding(horizontal = 10.dp)
     ) {
         item {
             val selectEntry = flowEngine.getEntrySelector()
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
index 818723b..5bc4796 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -19,11 +19,8 @@
 package com.android.credentialmanager.ui.screens.single.password
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
 import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.R
 import com.android.credentialmanager.ui.components.PasswordRow
@@ -67,7 +64,6 @@
             )
         },
         columnState = columnState,
-        modifier = Modifier.padding(horizontal = 10.dp)
     ) {
         item {
             Column {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
index 34d6e97..fd0fc8c 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
@@ -17,10 +17,7 @@
 package com.android.credentialmanager.ui.screens.single.signInWithProvider
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
 import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.ui.components.AccountRow
@@ -47,7 +44,6 @@
 fun SignInWithProviderScreen(
     entry: CredentialEntryInfo,
     columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
     flowEngine: FlowEngine,
 ) {
     SingleAccountScreen(
@@ -72,7 +68,6 @@
             }
         },
         columnState = columnState,
-        modifier = modifier.padding(horizontal = 10.dp)
     ) {
         item {
             val selectEntry = flowEngine.getEntrySelector()
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/EdgeToEdgeUtils.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/EdgeToEdgeUtils.java
index 6e53012..062e9b8 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/EdgeToEdgeUtils.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/EdgeToEdgeUtils.java
@@ -36,7 +36,7 @@
      * Enable Edge to Edge and handle overlaps using insets. It should be called before
      * setContentView.
      */
-    static void enable(@NonNull ComponentActivity activity) {
+    public static void enable(@NonNull ComponentActivity activity) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
             return;
         }
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 83a986b1..754d9423 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -9,16 +9,6 @@
 }
 
 flag {
-    name: "enable_cached_bluetooth_device_dedup"
-    namespace: "bluetooth"
-    description: "Enable dedup in CachedBluetoothDevice"
-    bug: "319197962"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
    name: "bluetooth_qs_tile_dialog_auto_on_toggle"
    namespace: "bluetooth"
    description: "Displays the auto on toggle in the bluetooth QS tile dialog"
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 5c4cdb2..687c728 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -881,6 +881,11 @@
     <!-- UI debug setting: show physical key presses summary [CHAR LIMIT=150] -->
     <string name="show_key_presses_summary">Show visual feedback for physical key presses</string>
 
+    <!-- UI debug setting: Title text for a debug setting that enables a visualization of touchpad input in a window on the screen  [CHAR LIMIT=50] -->
+    <string name="touchpad_visualizer">Show touchpad input</string>
+    <!-- UI debug setting: Summary text for a debug setting that enables a visualization of touchpad input in a window on the screen [CHAR LIMIT=150] -->
+    <string name="touchpad_visualizer_summary">Screen overlay displaying touchpad input data and recognized gestures</string>
+
     <!-- UI debug setting: show where surface updates happen? [CHAR LIMIT=25] -->
     <string name="show_screen_updates">Show surface updates</string>
     <!-- UI debug setting: show surface updates summary [CHAR LIMIT=50] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index e926b16..0dc772a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -16,8 +16,6 @@
 
 package com.android.settingslib.bluetooth;
 
-import static com.android.settingslib.flags.Flags.enableCachedBluetoothDeviceDedup;
-
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -403,7 +401,7 @@
                 cachedDevice = mDeviceManager.addDevice(device);
             }
 
-            if (enableCachedBluetoothDeviceDedup() && bondState == BluetoothDevice.BOND_BONDED) {
+            if (bondState == BluetoothDevice.BOND_BONDED) {
                 mDeviceManager.removeDuplicateInstanceForIdentityAddress(device);
             }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 2573907..de60fdc2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -578,7 +578,7 @@
     }
 
     /**
-     * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
+     * Check if {@link CachedBluetoothDevice} (lead or member) has connected to a broadcast source.
      *
      * @param cachedDevice The cached bluetooth device to check.
      * @param localBtManager The BT manager to provide BT functions.
@@ -586,20 +586,10 @@
      */
     @WorkerThread
     public static boolean hasConnectedBroadcastSource(
-            CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
-        if (localBtManager == null) {
-            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
-            return false;
-        }
-        LocalBluetoothLeBroadcastAssistant assistant =
-                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
-        if (assistant == null) {
-            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
-            return false;
-        }
-        List<BluetoothLeBroadcastReceiveState> sourceList =
-                assistant.getAllSources(cachedDevice.getDevice());
-        if (!sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected)) {
+            @Nullable CachedBluetoothDevice cachedDevice,
+            @Nullable LocalBluetoothManager localBtManager) {
+        if (cachedDevice == null) return false;
+        if (hasConnectedBroadcastSourceForBtDevice(cachedDevice.getDevice(), localBtManager)) {
             Log.d(
                     TAG,
                     "Lead device has connected broadcast source, device = "
@@ -608,9 +598,7 @@
         }
         // Return true if member device is in broadcast.
         for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
-            List<BluetoothLeBroadcastReceiveState> list =
-                    assistant.getAllSources(device.getDevice());
-            if (!list.isEmpty() && list.stream().anyMatch(BluetoothUtils::isConnected)) {
+            if (hasConnectedBroadcastSourceForBtDevice(device.getDevice(), localBtManager)) {
                 Log.d(
                         TAG,
                         "Member device has connected broadcast source, device = "
@@ -621,6 +609,28 @@
         return false;
     }
 
+    /**
+     * Check if {@link BluetoothDevice} has connected to a broadcast source.
+     *
+     * @param device The bluetooth device to check.
+     * @param localBtManager The BT manager to provide BT functions.
+     * @return Whether the device has connected to a broadcast source.
+     */
+    @WorkerThread
+    public static boolean hasConnectedBroadcastSourceForBtDevice(
+            @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) {
+        LocalBluetoothLeBroadcastAssistant assistant =
+                localBtManager == null
+                        ? null
+                        : localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
+        if (device == null || assistant == null) {
+            Log.d(TAG, "Skip check hasConnectedBroadcastSourceForBtDevice due to arg is null");
+            return false;
+        }
+        List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device);
+        return !sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected);
+    }
+
     /** Checks the connectivity status based on the provided broadcast receive state. */
     @WorkerThread
     public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
@@ -805,8 +815,10 @@
 
         ComponentName exclusiveManagerComponent =
                 ComponentName.unflattenFromString(exclusiveManagerName);
-        String exclusiveManagerPackage = exclusiveManagerComponent != null
-                ? exclusiveManagerComponent.getPackageName() : exclusiveManagerName;
+        String exclusiveManagerPackage =
+                exclusiveManagerComponent != null
+                        ? exclusiveManagerComponent.getPackageName()
+                        : exclusiveManagerName;
 
         if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) {
             return false;
diff --git a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
index d69c87b..4b6bcf9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
@@ -21,6 +21,7 @@
 import android.content.Intent
 import android.os.OutcomeReceiver
 import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteModemStateCallback
 import android.util.Log
 import android.view.WindowManager
 import androidx.lifecycle.LifecycleOwner
@@ -31,12 +32,19 @@
 import kotlinx.coroutines.Dispatchers.Default
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeoutException
 import kotlin.coroutines.resume
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOn
 
 /** A util for Satellite dialog */
 object SatelliteDialogUtils {
@@ -70,7 +78,7 @@
             coroutineScope.launch {
                 var isSatelliteModeOn = false
                 try {
-                    isSatelliteModeOn = requestIsEnabled(context)
+                    isSatelliteModeOn = requestIsSessionStarted(context)
                 } catch (e: InterruptedException) {
                     Log.w(TAG, "Error to get satellite status : $e")
                 } catch (e: ExecutionException) {
@@ -118,19 +126,107 @@
         }
 
         suspendCancellableCoroutine {continuation ->
-            satelliteManager?.requestIsEnabled(Default.asExecutor(),
-                    object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
-                        override fun onResult(result: Boolean) {
-                            Log.i(TAG, "Satellite modem enabled status: $result")
-                            continuation.resume(result)
-                        }
+            try {
+                satelliteManager?.requestIsEnabled(Default.asExecutor(),
+                        object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+                            override fun onResult(result: Boolean) {
+                                Log.i(TAG, "Satellite modem enabled status: $result")
+                                continuation.resume(result)
+                            }
 
-                        override fun onError(error: SatelliteManager.SatelliteException) {
-                            super.onError(error)
-                            Log.w(TAG, "Can't get satellite modem enabled status", error)
-                            continuation.resume(false)
-                        }
-                    })
+                            override fun onError(error: SatelliteManager.SatelliteException) {
+                                super.onError(error)
+                                Log.w(TAG, "Can't get satellite modem enabled status", error)
+                                continuation.resume(false)
+                            }
+                        })
+            } catch (e: IllegalStateException) {
+                Log.w(TAG, "IllegalStateException: $e")
+                continuation.resume(false)
+            }
+        }
+    }
+
+    private suspend fun requestIsSessionStarted(
+            context: Context
+    ): Boolean = withContext(Default) {
+        val satelliteManager: SatelliteManager? =
+                context.getSystemService(SatelliteManager::class.java)
+        if (satelliteManager == null) {
+            Log.w(TAG, "SatelliteManager is null")
+            return@withContext false
+        }
+
+        getIsSessionStartedFlow(context).conflate().first()
+    }
+
+    /**
+     * Provides a Flow that emits the session state of the satellite modem. Updates are triggered
+     * when the modem state changes.
+     *
+     * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`).
+     * @return A Flow emitting `true` when the session is started and `false` otherwise.
+     */
+    private fun getIsSessionStartedFlow(
+            context: Context
+    ): Flow<Boolean> {
+        val satelliteManager: SatelliteManager? =
+                context.getSystemService(SatelliteManager::class.java)
+        if (satelliteManager == null) {
+            Log.w(TAG, "SatelliteManager is null")
+            return flowOf(false)
+        }
+
+        return callbackFlow {
+            val callback = SatelliteModemStateCallback { state ->
+                val isSessionStarted = isSatelliteSessionStarted(state)
+                Log.i(TAG, "Satellite modem state changed: state=$state"
+                        + ", isSessionStarted=$isSessionStarted")
+                trySend(isSessionStarted)
+            }
+
+            var registerResult = SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+            try {
+                registerResult = satelliteManager.registerForModemStateChanged(
+                        Default.asExecutor(),
+                        callback
+                )
+            } catch (e: IllegalStateException) {
+                Log.w(TAG, "IllegalStateException: $e")
+            }
+
+
+            if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
+                // If the registration failed (e.g., device doesn't support satellite),
+                // SatelliteManager will not emit the current state by callback.
+                // We send `false` value by ourself to make sure the flow has initial value.
+                Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
+                trySend(false)
+            }
+
+            awaitClose {
+                try {
+                    satelliteManager.unregisterForModemStateChanged(callback)
+                } catch (e: IllegalStateException) {
+                    Log.w(TAG, "IllegalStateException: $e")
+                }
+            }
+        }.flowOn(Default)
+    }
+
+
+    /**
+     * Check if the modem is in a satellite session.
+     *
+     * @param state The SatelliteModemState provided by the SatelliteManager.
+     * @return `true` if the modem is in a satellite session, `false` otherwise.
+     */
+    fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean {
+        return when (state) {
+            SatelliteManager.SATELLITE_MODEM_STATE_OFF,
+            SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE,
+            SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false
+            else -> true
         }
     }
 
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 370d568..4551f1e 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
@@ -56,7 +56,10 @@
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
 public class BluetoothUtilsTest {
@@ -557,8 +560,14 @@
     }
 
     @Test
-    public void testHasConnectedBroadcastSource_deviceConnectedToBroadcastSource() {
+    public void testHasConnectedBroadcastSource_leadDeviceConnectedToBroadcastSource() {
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
+        BluetoothDevice memberDevice = mock(BluetoothDevice.class);
+        when(memberCachedDevice.getDevice()).thenReturn(memberDevice);
+        Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>();
+        memberCachedDevices.add(memberCachedDevice);
+        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices);
 
         List<Long> bisSyncState = new ArrayList<>();
         bisSyncState.add(1L);
@@ -566,7 +575,8 @@
 
         List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
         sourceList.add(mLeBroadcastReceiveState);
-        when(mAssistant.getAllSources(any())).thenReturn(sourceList);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+        when(mAssistant.getAllSources(memberDevice)).thenReturn(Collections.emptyList());
 
         assertThat(
                         BluetoothUtils.hasConnectedBroadcastSource(
@@ -575,6 +585,79 @@
     }
 
     @Test
+    public void testHasConnectedBroadcastSource_memberDeviceConnectedToBroadcastSource() {
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class);
+        BluetoothDevice memberDevice = mock(BluetoothDevice.class);
+        when(memberCachedDevice.getDevice()).thenReturn(memberDevice);
+        Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>();
+        memberCachedDevices.add(memberCachedDevice);
+        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices);
+
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(memberDevice)).thenReturn(sourceList);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(Collections.emptyList());
+
+        assertThat(
+                        BluetoothUtils.hasConnectedBroadcastSource(
+                                mCachedBluetoothDevice, mLocalBluetoothManager))
+                .isTrue();
+    }
+
+    @Test
+    public void testHasConnectedBroadcastSource_deviceNotConnectedToBroadcastSource() {
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+
+        List<Long> bisSyncState = new ArrayList<>();
+        when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                        BluetoothUtils.hasConnectedBroadcastSource(
+                                mCachedBluetoothDevice, mLocalBluetoothManager))
+                .isFalse();
+    }
+
+    @Test
+    public void testHasConnectedBroadcastSourceForBtDevice_deviceConnectedToBroadcastSource() {
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                        BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(
+                                mBluetoothDevice, mLocalBluetoothManager))
+                .isTrue();
+    }
+
+    @Test
+    public void testHasConnectedBroadcastSourceForBtDevice_deviceNotConnectedToBroadcastSource() {
+        List<Long> bisSyncState = new ArrayList<>();
+        when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+        List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+        sourceList.add(mLeBroadcastReceiveState);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList);
+
+        assertThat(
+                        BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(
+                                mBluetoothDevice, mLocalBluetoothManager))
+                .isFalse();
+    }
+
+    @Test
     public void isAvailableHearingDevice_isConnectedHearingAid_returnTure() {
         when(mCachedBluetoothDevice.isConnectedHearingAidDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
@@ -604,9 +687,19 @@
     }
 
     @Test
-    public void getSecondaryDeviceForBroadcast_errorState_returnNull() {
-        assertThat(BluetoothUtils.getSecondaryDeviceForBroadcast(mContext.getContentResolver(),
-                mLocalBluetoothManager))
+    public void getSecondaryDeviceForBroadcast_noBroadcast_returnNull() {
+        assertThat(
+                        BluetoothUtils.getSecondaryDeviceForBroadcast(
+                                mContext.getContentResolver(), mLocalBluetoothManager))
+                .isNull();
+    }
+
+    @Test
+    public void getSecondaryDeviceForBroadcast_nullProfile_returnNull() {
+        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(null);
+        assertThat(
+                        BluetoothUtils.getSecondaryDeviceForBroadcast(
+                                mContext.getContentResolver(), mLocalBluetoothManager))
                 .isNull();
     }
 
@@ -616,6 +709,7 @@
                 mContext.getContentResolver(),
                 BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
                 1);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
         CachedBluetoothDeviceManager deviceManager = mock(CachedBluetoothDeviceManager.class);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(deviceManager);
         when(deviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice);
@@ -625,8 +719,9 @@
         when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(state));
         when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mBluetoothDevice));
 
-        assertThat(BluetoothUtils.getSecondaryDeviceForBroadcast(mContext.getContentResolver(),
-                mLocalBluetoothManager))
+        assertThat(
+                        BluetoothUtils.getSecondaryDeviceForBroadcast(
+                                mContext.getContentResolver(), mLocalBluetoothManager))
                 .isNull();
     }
 
@@ -636,6 +731,7 @@
                 mContext.getContentResolver(),
                 BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
                 1);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
         CachedBluetoothDeviceManager deviceManager = mock(CachedBluetoothDeviceManager.class);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(deviceManager);
         CachedBluetoothDevice cachedBluetoothDevice = mock(CachedBluetoothDevice.class);
@@ -655,8 +751,9 @@
         when(mAssistant.getAllConnectedDevices())
                 .thenReturn(ImmutableList.of(mBluetoothDevice, bluetoothDevice));
 
-        assertThat(BluetoothUtils.getSecondaryDeviceForBroadcast(mContext.getContentResolver(),
-                mLocalBluetoothManager))
+        assertThat(
+                        BluetoothUtils.getSecondaryDeviceForBroadcast(
+                                mContext.getContentResolver(), mLocalBluetoothManager))
                 .isEqualTo(mCachedBluetoothDevice);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
index aeda1ed6..2078b36 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
@@ -17,11 +17,12 @@
 package com.android.settingslib.satellite
 
 import android.content.Context
-import android.content.Intent
-import android.os.OutcomeReceiver
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.telephony.satellite.SatelliteManager
-import android.telephony.satellite.SatelliteManager.SatelliteException
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_ENABLING_SATELLITE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_MODEM_ERROR
+import android.telephony.satellite.SatelliteModemStateCallback
 import android.util.AndroidRuntimeException
 import androidx.test.core.app.ApplicationProvider
 import com.android.internal.telephony.flags.Flags
@@ -67,26 +68,19 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteIsOn_showWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
                 .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
-                                    1
-                            )
-                    receiver.onResult(true)
+                    val callback = invocation
+                            .getArgument<SatelliteModemStateCallback>(1)
+                    callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_ENABLING_SATELLITE)
                     null
                 }
 
         try {
             SatelliteDialogUtils.mayStartSatelliteWarningDialog(
                     context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                        assertTrue(it)
-                })
+                assertTrue(it)
+            })
         } catch (e: AndroidRuntimeException) {
             // Catch exception of starting activity .
         }
@@ -95,68 +89,61 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteIsOff_notShowWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
                 .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
-                                    1
-                            )
-                    receiver.onResult(false)
+                    val callback = invocation
+                            .getArgument<SatelliteModemStateCallback>(1)
+                    callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF)
                     null
                 }
 
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
     }
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_noSatelliteManager_notShowWarningDialog() = runBlocking {
-        `when`(context.getSystemService(SatelliteManager::class.java))
-                .thenReturn(null)
+        `when`(context.getSystemService(SatelliteManager::class.java)).thenReturn(null)
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
     }
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteErrorResult_notShowWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
-                .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
-                                    1
-                            )
-                    receiver.onError(SatelliteException(SatelliteManager.SATELLITE_RESULT_ERROR))
-                    null
-                }
-
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
+                .thenReturn(SATELLITE_RESULT_MODEM_ERROR)
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    fun mayStartSatelliteWarningDialog_phoneCrash_notShowWarningDialog() = runBlocking {
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
+                .thenThrow(IllegalStateException("Telephony is null!!!"))
+
+        SatelliteDialogUtils.mayStartSatelliteWarningDialog(
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
+
+        verify(context, Times(0)).startActivity(any())
     }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 7b927d7..2823277 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -200,6 +200,7 @@
         VALIDATORS.put(System.POINTER_LOCATION, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.SHOW_TOUCHES, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.SHOW_KEY_PRESSES, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.TOUCHPAD_VISUALIZER, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.SHOW_ROTARY_INPUT, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.WINDOW_ORIENTATION_LISTENER_LOG, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.LOCKSCREEN_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index cd37ad1..3c24f5c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2831,6 +2831,9 @@
                 Settings.System.SHOW_KEY_PRESSES,
                 SystemSettingsProto.DevOptions.SHOW_KEY_PRESSES);
         dumpSetting(s, p,
+                Settings.System.TOUCHPAD_VISUALIZER,
+                SystemSettingsProto.DevOptions.TOUCHPAD_VISUALIZER);
+        dumpSetting(s, p,
                 Settings.System.POINTER_LOCATION,
                 SystemSettingsProto.DevOptions.POINTER_LOCATION);
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index e8ef620..ba59ce8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3264,6 +3264,24 @@
 
             if (forceNotify || success) {
                 notifyForSettingsChange(key, name);
+
+                // If this is an aconfig flag, it will be written as a staged flag.
+                // Notify that its staged flag value will be updated.
+                if (Flags.notifyIndividualAconfigSyspropChanged() && type == SETTINGS_TYPE_CONFIG) {
+                    int slashIndex = name.indexOf('/');
+                    boolean validSlashIndex = slashIndex != -1
+                            && slashIndex != 0
+                            && slashIndex != name.length();
+                    if (validSlashIndex) {
+                        String namespace = name.substring(0, slashIndex);
+                        String flagName = name.substring(slashIndex + 1);
+                        if (settingsState.getAconfigDefaultFlags().containsKey(flagName)) {
+                            String stagedName = "staged/" + namespace + "*" + flagName;
+                            notifyForSettingsChange(key, stagedName);
+                        }
+                    }
+                }
+
                 if (wasUnsetNonPredefinedSetting) {
                     // Increment the generation number for all non-predefined, unset settings,
                     // because a new non-predefined setting has been inserted
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index 4f5955b..f53dec6 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -52,3 +52,14 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "notify_individual_aconfig_sysprop_changed"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, propagate individual aconfig sys props on flag stage."
+    bug: "331963764"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 411decd..8c96484 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -918,6 +918,7 @@
                         Settings.System.SHOW_GTALK_SERVICE_STATUS, // candidate for backup?
                         Settings.System.SHOW_TOUCHES,
                         Settings.System.SHOW_KEY_PRESSES,
+                        Settings.System.TOUCHPAD_VISUALIZER,
                         Settings.System.SHOW_ROTARY_INPUT,
                         Settings.System.SIP_ADDRESS_ONLY, // value, not a setting
                         Settings.System.SIP_ALWAYS, // value, not a setting
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cfd8f635..c2e8c37 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -576,6 +576,7 @@
         "TraceurCommon",
         "Traceur-res",
         "//frameworks/libs/systemui:motion_tool_lib",
+        "//frameworks/libs/systemui:contextualeducationlib",
         "notification_flags_lib",
         "PlatformComposeCore",
         "PlatformComposeSceneTransitionLayout",
@@ -736,6 +737,7 @@
         "WindowManager-Shell",
         "LowLightDreamLib",
         "//frameworks/libs/systemui:motion_tool_lib",
+        "//frameworks/libs/systemui:contextualeducationlib",
         "androidx.core_core-animation-testing",
         "androidx.compose.ui_ui",
         "flag-junit",
diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig
index bd1a442..e81d5d5 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -4,13 +4,6 @@
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
 flag {
-    name: "bp_talkback"
-    namespace: "biometrics_framework"
-    description: "Adds talkback directional guidance when using UDFPS with biometric prompt"
-    bug: "310044658"
-}
-
-flag {
     name: "constraint_bp"
     namespace: "biometrics_framework"
     description: "Refactors Biometric Prompt to use a ConstraintLayout"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0cbaf29..201aaed 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -403,6 +403,13 @@
 }
 
 flag {
+    name: "status_bar_call_chip_notification_icon"
+    namespace: "systemui"
+    description: "Use the small icon set on the notification for the status bar call chip"
+    bug: "354930838"
+}
+
+flag {
     name: "compose_bouncer"
     namespace: "systemui"
     description: "Use the new compose bouncer in SystemUI"
@@ -582,16 +589,6 @@
 }
 
 flag {
-    name: "screenshot_private_profile_accessibility_announcement_fix"
-    namespace: "systemui"
-    description: "Modified a11y announcement for private space screenshots"
-    bug: "326941376"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "screenshot_private_profile_behavior_fix"
     namespace: "systemui"
     description: "Private profile support for screenshots"
@@ -1301,3 +1298,10 @@
         purpose: PURPOSE_BUGFIX
    }
 }
+
+flag {
+   name: "compose_haptic_sliders"
+   namespace: "systemui"
+   description: "Adding haptic component infrastructure to sliders in Compose."
+   bug: "341968766"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 7a41bc6..1255248 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -99,7 +99,9 @@
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
 import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
@@ -741,6 +743,18 @@
         }
 
     @Test
+    fun communalContent_readTriggersUmoVisibilityUpdate() =
+        testScope.runTest {
+            verify(mediaHost, never()).updateViewVisibility()
+
+            val communalContent by collectLastValue(underTest.communalContent)
+
+            // updateViewVisibility is called when the flow is collected.
+            assertThat(communalContent).isNotNull()
+            verify(mediaHost).updateViewVisibility()
+        }
+
+    @Test
     fun scrollPosition_persistedOnEditEntry() {
         val index = 2
         val offset = 30
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 86c680a..023de52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -82,7 +82,7 @@
                 assertFalse(launching!!)
 
                 val parent = FrameLayout(context)
-                val view = CommunalAppWidgetHostView(context)
+                val view = CommunalAppWidgetHostView(context, underTest)
                 parent.addView(view)
                 val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
index 3a4b14b..331db52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
@@ -22,10 +22,10 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.SysuiTestableContext
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shared.education.GestureType.BACK_GESTURE
 import com.google.common.truth.Truth.assertThat
 import java.io.File
 import java.time.Clock
@@ -70,8 +70,8 @@
     fun changeRetrievedValueForNewUser() =
         testScope.runTest {
             // Update data for old user.
-            underTest.incrementSignalCount(BACK_GESTURE)
-            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE))
+            underTest.incrementSignalCount(BACK)
+            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
             assertThat(model?.signalCount).isEqualTo(1)
 
             // User is changed.
@@ -83,17 +83,17 @@
     @Test
     fun incrementSignalCount() =
         testScope.runTest {
-            underTest.incrementSignalCount(BACK_GESTURE)
-            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE))
+            underTest.incrementSignalCount(BACK)
+            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
             assertThat(model?.signalCount).isEqualTo(1)
         }
 
     @Test
     fun dataAddedOnUpdateShortcutTriggerTime() =
         testScope.runTest {
-            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE))
+            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
             assertThat(model?.lastShortcutTriggeredTime).isNull()
-            underTest.updateShortcutTriggerTime(BACK_GESTURE)
+            underTest.updateShortcutTriggerTime(BACK)
             assertThat(model?.lastShortcutTriggeredTime).isEqualTo(clock.instant())
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 01dbc6b..ae3302c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -20,10 +20,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.education.data.repository.contextualEducationRepository
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shared.education.GestureType
-import com.android.systemui.shared.education.GestureType.BACK_GESTURE
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -47,15 +47,15 @@
     @Test
     fun newEducationInfoOnMaxSignalCountReached() =
         testScope.runTest {
-            tryTriggeringEducation(BACK_GESTURE)
+            tryTriggeringEducation(BACK)
             val model by collectLastValue(underTest.educationTriggered)
-            assertThat(model?.gestureType).isEqualTo(BACK_GESTURE)
+            assertThat(model?.gestureType).isEqualTo(BACK)
         }
 
     @Test
     fun noEducationInfoBeforeMaxSignalCountReached() =
         testScope.runTest {
-            repository.incrementSignalCount(BACK_GESTURE)
+            repository.incrementSignalCount(BACK)
             val model by collectLastValue(underTest.educationTriggered)
             assertThat(model).isNull()
         }
@@ -64,8 +64,8 @@
     fun noEducationInfoWhenShortcutTriggeredPreviously() =
         testScope.runTest {
             val model by collectLastValue(underTest.educationTriggered)
-            repository.updateShortcutTriggerTime(BACK_GESTURE)
-            tryTriggeringEducation(BACK_GESTURE)
+            repository.updateShortcutTriggerTime(BACK)
+            tryTriggeringEducation(BACK)
             assertThat(model).isNull()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
index ee51e37..cd0c58f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
@@ -20,10 +20,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.education.data.repository.contextualEducationRepository
 import com.android.systemui.education.data.repository.fakeEduClock
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shared.education.GestureType.BACK_GESTURE
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -41,11 +41,9 @@
     fun dataUpdatedOnIncrementSignalCount() =
         testScope.runTest {
             val model by
-                collectLastValue(
-                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
-                )
+                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
             val originalValue = model!!.signalCount
-            underTest.incrementSignalCount(BACK_GESTURE)
+            underTest.incrementSignalCount(BACK)
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
 
@@ -53,11 +51,9 @@
     fun dataAddedOnUpdateShortcutTriggerTime() =
         testScope.runTest {
             val model by
-                collectLastValue(
-                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
-                )
+                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
             assertThat(model?.lastShortcutTriggeredTime).isNull()
-            underTest.updateShortcutTriggerTime(BACK_GESTURE)
+            underTest.updateShortcutTriggerTime(BACK)
             assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
         }
 }
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 a8eccc5..03647b9 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
@@ -136,30 +136,6 @@
         }
 
     @Test
-    fun finishedKeyguardTransitionStepTests() =
-        testScope.runTest {
-            val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep)
-            val steps = mutableListOf<TransitionStep>()
-
-            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
-            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
-            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
-            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
-            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
-            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
-            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
-            steps.forEach {
-                repository.sendTransitionStep(it)
-                runCurrent()
-            }
-
-            // Ignore the default state.
-            assertThat(finishedSteps.subList(1, finishedSteps.size))
-                .isEqualTo(listOf(steps[2], steps[5]))
-        }
-
-    @Test
     fun startedKeyguardTransitionStepTests() =
         testScope.runTest {
             val startedSteps by collectValues(underTest.startedKeyguardTransitionStep)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index cde703b..5e9badc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -156,6 +156,43 @@
             assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(1f)
         }
 
+    @Test
+    fun deviceEntryBackgroundView_onCancel() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsUdfps()
+            val deviceEntryBackgroundViewAlpha by
+                collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
+
+            // GIVEN transition START
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+            // WHEN transition is cancelled
+            repository.sendTransitionStep(step(.1f, TransitionState.CANCELED))
+
+            // THEN alpha is immediately set to 1f (expected lockscreen alpha state)
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(1f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_onCancel() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsUdfps()
+            val deviceEntryBackgroundViewAlpha by
+                collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
+
+            // GIVEN transition START
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+            // WHEN transition is cancelled
+            repository.sendTransitionStep(step(.1f, TransitionState.CANCELED))
+
+            // THEN alpha is immediately set to 1f (expected lockscreen alpha state)
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(1f)
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 194f362..6dbe94b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -19,11 +19,13 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.communalSceneRepository
@@ -68,6 +70,11 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
+@EnableFlags(
+    FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+    FLAG_NEW_AOD_TRANSITION,
+    FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+)
 class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
@@ -102,13 +109,6 @@
 
     @Before
     fun setUp() {
-        mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION)
-        if (!SceneContainerFlag.isEnabled) {
-            mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-            mSetFlagsRule.disableFlags(
-                AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-            )
-        }
         kosmos.sceneContainerRepository.setTransitionState(transitionState)
     }
 
@@ -212,6 +212,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope,
+            )
             notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(false)
@@ -227,6 +232,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope,
+            )
             notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
@@ -243,6 +253,11 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope,
+            )
             notificationsKeyguardInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
@@ -255,6 +270,27 @@
         }
 
     @Test
+    fun iconContainer_isNotVisible_bypassDisabled_onLockscreen() =
+        testScope.runTest {
+            val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+            runCurrent()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+            notificationsKeyguardInteractor.setPulseExpanding(false)
+            deviceEntryRepository.setBypassEnabled(false)
+            whenever(dozeParameters.alwaysOn).thenReturn(true)
+            whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
+            notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
+            runCurrent()
+
+            assertThat(isVisible?.value).isFalse()
+            assertThat(isVisible?.isAnimating).isTrue()
+        }
+
+    @Test
     fun isIconContainerVisible_stopAnimation() =
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 6ce7e88..e6ea64f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -201,6 +201,24 @@
             assertThat(actual).isEqualTo(0f)
         }
 
+    @Test
+    fun deviceEntryParentViewAlpha_shadeNotExpanded_onCancel() =
+        testScope.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
+            // START transition
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(1f)
+
+            // WHEN transition is canceled
+            repository.sendTransitionStep(step(1f, TransitionState.CANCELED))
+
+            // THEN alpha is immediately set to 0f
+            assertThat(actual).isEqualTo(0f)
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 34fbcac..40fb769 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -60,7 +60,6 @@
 import com.android.systemui.res.R;
 import com.android.systemui.scene.FakeWindowRootViewComponent;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -107,7 +106,6 @@
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
     @Mock private UserTracker mUserTracker;
-    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
@@ -163,7 +161,7 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                mNotificationShadeWindowModel,
+                mKosmos.getNotificationShadeWindowModel(),
                 mKosmos::getCommunalInteractor) {
                     @Override
                     protected boolean isDebuggable() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
index aef9163..b917014 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
@@ -124,4 +124,36 @@
             underTest.setIsLaunchingActivity(true)
             Truth.assertThat(underTest.isLaunchingActivity.value).isEqualTo(true)
         }
+
+    @Test
+    fun isAnyFlingAnimationRunning() =
+        testScope.runTest() {
+            val actual by collectLastValue(underTest.isAnyFlingAnimationRunning)
+
+            // WHEN transitioning from QS to Gone with user input ongoing
+            val userInputOngoing = MutableStateFlow(true)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = Scenes.QuickSettings,
+                        toScene = Scenes.Gone,
+                        currentScene = flowOf(Scenes.QuickSettings),
+                        progress = MutableStateFlow(.1f),
+                        isInitiatedByUserInput = true,
+                        isUserInputOngoing = userInputOngoing,
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN qs is not flinging
+            Truth.assertThat(actual).isFalse()
+
+            // WHEN user input ends
+            userInputOngoing.value = false
+            runCurrent()
+
+            // THEN qs is flinging
+            Truth.assertThat(actual).isTrue()
+        }
 }
diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
index e6ae2e1..2a00495 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
@@ -87,20 +87,21 @@
                 android:id="@+id/logo_description"
                 style="@style/TextAppearance.AuthCredential.LogoDescription"
                 android:layout_width="0dp"
-                android:layout_height="wrap_content"
+                android:layout_height="@dimen/biometric_prompt_logo_size"
+                android:gravity="start|center_vertical"
                 android:textAlignment="viewStart"
-                android:paddingStart="16dp"
-                app:layout_constraintBottom_toBottomOf="@+id/logo"
+                android:layout_marginStart="16dp"
+                app:layout_goneMarginStart="0dp"
+                app:layout_constraintBottom_toTopOf="@+id/title"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toEndOf="@+id/logo"
-                app:layout_constraintTop_toTopOf="@+id/logo" />
+                app:layout_constraintTop_toTopOf="parent" />
 
             <TextView
                 android:id="@+id/title"
                 style="@style/TextAppearance.AuthCredential.Title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="12dp"
                 android:gravity="@integer/biometric_dialog_text_gravity"
                 android:paddingHorizontal="0dp"
                 android:textAlignment="viewStart"
@@ -108,7 +109,7 @@
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintHorizontal_bias="0.0"
                 app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/logo"
+                app:layout_constraintTop_toBottomOf="@+id/logoBarrier"
                 app:layout_constraintVertical_bias="0.0"
                 app:layout_constraintVertical_chainStyle="packed" />
 
@@ -159,6 +160,15 @@
                 app:layout_constraintVertical_bias="0.0" />
 
             <androidx.constraintlayout.widget.Barrier
+                android:id="@+id/logoBarrier"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:barrierMargin="12dp"
+                app:barrierAllowsGoneWidgets="false"
+                app:barrierDirection="bottom"
+                app:constraint_referenced_ids="logo_description, logo" />
+
+            <androidx.constraintlayout.widget.Barrier
                 android:id="@+id/contentBarrier"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/hearing_tool_item.xml b/packages/SystemUI/res/layout/hearing_tool_item.xml
index 84462d0..ff2fbe07 100644
--- a/packages/SystemUI/res/layout/hearing_tool_item.xml
+++ b/packages/SystemUI/res/layout/hearing_tool_item.xml
@@ -20,7 +20,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:gravity="center"
+    android:gravity="top|center_horizontal"
     android:focusable="true"
     android:clickable="true"
     android:layout_weight="1">
@@ -46,8 +46,8 @@
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/hearing_devices_layout_margin"
         android:ellipsize="end"
-        android:maxLines="1"
         android:textSize="12sp"
+        android:maxLines="3"
         android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
         android:focusable="false" />
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index c29c236..f0c8894 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1050,4 +1050,7 @@
 
     <!-- Only applicable for dual shade - Allow Notifications/QS shade to anchor to the bottom. -->
     <bool name="config_dualShadeAlignedToBottom">false</bool>
+
+    <!-- List of packages for which we want to use activity info (instead of application info) for biometric prompt logo. Empty for AOSP. [DO NOT TRANSLATE] -->
+    <string-array name="config_useActivityLogoForBiometricPrompt" translatable="false"/>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index eda7bb0..e5750d2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1729,10 +1729,18 @@
     <dimen name="wallet_button_vertical_padding">8dp</dimen>
 
     <!-- Ongoing activity chip -->
+    <!-- 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>
+    <!-- The icon size, used with the default phone icon. -->
     <dimen name="ongoing_activity_chip_icon_size">16dp</dimen>
-    <!-- The padding between the icon and the text. -->
+    <!-- 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.) -->
+    <dimen name="ongoing_activity_chip_embedded_padding_icon_size">22dp</dimen>
+    <!-- The padding between the icon and the text. Only used if the default phone icon is used. -->
     <dimen name="ongoing_activity_chip_icon_text_padding">4dp</dimen>
+    <!-- The end padding for the timer text view. Only used if an embedded padding icon is used. -->
+    <dimen name="ongoing_activity_chip_text_end_padding_for_embedded_padding_icon">6dp</dimen>
     <dimen name="ongoing_activity_chip_corner_radius">28dp</dimen>
 
     <!-- Status bar user chip -->
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 212dae2..e4f900d 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -132,6 +132,7 @@
 
     <!-- Status bar -->
     <item type="id" name="status_bar_dot" />
+    <item type="id" name="ongoing_activity_chip_custom_icon" />
 
     <!-- Default display cutout on the physical top of screen -->
     <item type="id" name="display_cutout" />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e590f15..fe49f3a 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -432,8 +432,6 @@
 
     <!-- Content description for the app logo icon on biometric prompt. [CHAR LIMIT=NONE] -->
     <string name="biometric_dialog_logo">App logo</string>
-    <!-- List of packages for which we want to show overridden logo. For example, an app overrides its launcher logo, if it's in this array, biometric dialog shows the overridden logo; otherwise biometric dialog still shows the default application info icon. [CHAR LIMIT=NONE] -->
-    <string-array name="biometric_dialog_package_names_for_logo_with_overrides" />
     <!-- Message shown when a biometric is authenticated, asking the user to confirm authentication [CHAR LIMIT=30] -->
     <string name="biometric_dialog_confirm">Confirm</string>
     <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR LIMIT=30] -->
@@ -1381,11 +1379,15 @@
 
     <!-- Casting that launched by SysUI (i.e. when there is no app name) -->
     <!-- System casting media projection permission dialog title. [CHAR LIMIT=100] -->
-    <string name="media_projection_entry_cast_permission_dialog_title">Start casting?</string>
+    <string name="media_projection_entry_cast_permission_dialog_title">Cast your screen?</string>
+    <!-- System casting media projection permission option for capturing just a single app [CHAR LIMIT=50] -->
+    <string name="media_projection_entry_cast_permission_dialog_option_text_single_app">Cast one app</string>
+    <!-- System casting media projection permission option for capturing the whole screen [CHAR LIMIT=50] -->
+    <string name="media_projection_entry_cast_permission_dialog_option_text_entire_screen">Cast entire screen</string>
     <!-- System casting media projection permission warning for capturing the whole screen when SysUI casting requests it. [CHAR LIMIT=350] -->
-    <string name="media_projection_entry_cast_permission_dialog_warning_entire_screen">When you’re casting, Android has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+    <string name="media_projection_entry_cast_permission_dialog_warning_entire_screen">When you’re casting your entire screen, anything on your screen is visible. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
     <!-- System casting media projection permission warning for capturing a single app when SysUI casting requests it. [CHAR LIMIT=350] -->
-    <string name="media_projection_entry_cast_permission_dialog_warning_single_app">When you’re casting an app, Android has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+    <string name="media_projection_entry_cast_permission_dialog_warning_single_app">When you’re casting an app, anything shown or played in that app is visible. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
     <!-- System casting media projection permission button to continue for SysUI casting. [CHAR LIMIT=60] -->
     <string name="media_projection_entry_cast_permission_dialog_continue">Start casting</string>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index c428705d..7fa7088 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -192,11 +192,10 @@
 
     <style name="TextAppearance.AuthCredential.LogoDescription" parent="TextAppearance.Material3.LabelLarge" >
         <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
-        <item name="android:ellipsize">marquee</item>
         <item name="android:gravity">@integer/biometric_dialog_text_gravity</item>
-        <item name="android:marqueeRepeatLimit">1</item>
-        <item name="android:singleLine">true</item>
+        <item name="android:maxLines">1</item>
         <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+        <item name="android:ellipsize">end</item>
     </style>
 
     <style name="TextAppearance.AuthCredential.Title" parent="TextAppearance.Material3.HeadlineSmall" >
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 0352da3..9521be1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -412,7 +412,6 @@
         });
 
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-        setFocusableInTouchMode(true);
         requestFocus();
     }
 
@@ -464,7 +463,8 @@
         }
     }
 
-    private void onBackInvoked() {
+    @VisibleForTesting
+    public void onBackInvoked() {
         sendEarlyUserCanceled();
         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
index ca03a00..da270c0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
 import com.android.systemui.biometrics.shared.model.AuthenticationState
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
@@ -49,6 +48,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterIsInstance
@@ -85,7 +85,7 @@
      *   onAcquired in [FingerprintManager.EnrollmentCallback] and [FaceManager.EnrollmentCallback]
      */
     private val authenticationState: Flow<AuthenticationState> =
-        conflatedCallbackFlow {
+        callbackFlow {
                 val updateAuthenticationState = { state: AuthenticationState ->
                     Log.d(TAG, "authenticationState updated: $state")
                     trySendWithFailureLogging(state, TAG, "Error sending AuthenticationState state")
@@ -169,7 +169,9 @@
                         }
                     }
 
-                updateAuthenticationState(AuthenticationState.Idle(AuthenticationReason.NotRunning))
+                updateAuthenticationState(
+                    AuthenticationState.Idle(requestReason = AuthenticationReason.NotRunning)
+                )
                 biometricManager?.registerAuthenticationStateListener(authenticationStateListener)
                 awaitClose {
                     biometricManager?.unregisterAuthenticationStateListener(
@@ -180,23 +182,32 @@
             .distinctUntilChanged()
             .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
 
-    override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+    private val fingerprintAuthenticationState: Flow<AuthenticationState> =
         authenticationState
             .filter {
-                it is AuthenticationState.Idle ||
-                    (it is AuthenticationState.Started &&
-                        it.biometricSourceType == BiometricSourceType.FINGERPRINT) ||
-                    (it is AuthenticationState.Stopped &&
-                        it.biometricSourceType == BiometricSourceType.FINGERPRINT)
+                it.biometricSourceType == null ||
+                    it.biometricSourceType == BiometricSourceType.FINGERPRINT
             }
+            .onEach { Log.d(TAG, "fingerprintAuthenticationState updated: $it") }
+
+    private val fingerprintRunningState: Flow<AuthenticationState> =
+        fingerprintAuthenticationState
+            .filter {
+                it is AuthenticationState.Idle ||
+                    it is AuthenticationState.Started ||
+                    it is AuthenticationState.Stopped
+            }
+            .onEach { Log.d(TAG, "fingerprintRunningState updated: $it") }
+
+    override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+        fingerprintRunningState
             .map { it.requestReason }
             .onEach { Log.d(TAG, "fingerprintAuthenticationReason updated: $it") }
 
     override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
-        authenticationState
-            .filterIsInstance<AuthenticationState.Acquired>()
-            .filter { it.biometricSourceType == BiometricSourceType.FINGERPRINT }
-            .map { AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo) }
+        fingerprintAuthenticationState.filterIsInstance<AuthenticationState.Acquired>().map {
+            AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo)
+        }
 
     companion object {
         private const val TAG = "BiometricStatusRepositoryImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
index 5ceae36..81ea6a9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
@@ -27,6 +27,9 @@
  * authentication.
  */
 sealed interface AuthenticationState {
+    /** Indicates [BiometricSourceType] of authentication state update, null in idle auth state. */
+    val biometricSourceType: BiometricSourceType?
+
     /**
      * Indicates [AuthenticationReason] from [BiometricRequestConstants.RequestReason] for
      * requesting auth
@@ -43,7 +46,7 @@
      *   message.
      */
     data class Acquired(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason,
         val acquiredInfo: Int
     ) : AuthenticationState
@@ -59,7 +62,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Error(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val errString: String?,
         val errCode: Int,
         override val requestReason: AuthenticationReason,
@@ -73,7 +76,7 @@
      * @param userId The user id for the requested authentication
      */
     data class Failed(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason,
         val userId: Int
     ) : AuthenticationState
@@ -87,7 +90,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Help(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val helpString: String?,
         val helpCode: Int,
         override val requestReason: AuthenticationReason,
@@ -96,9 +99,13 @@
     /**
      * Authentication state when no auth is running
      *
+     * @param biometricSourceType null
      * @param requestReason [AuthenticationReason.NotRunning]
      */
-    data class Idle(override val requestReason: AuthenticationReason) : AuthenticationState
+    data class Idle(
+        override val biometricSourceType: BiometricSourceType? = null,
+        override val requestReason: AuthenticationReason
+    ) : AuthenticationState
 
     /**
      * AuthenticationState when auth is started
@@ -107,7 +114,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Started(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason
     ) : AuthenticationState
 
@@ -118,7 +125,7 @@
      * @param requestReason [AuthenticationReason.NotRunning]
      */
     data class Stopped(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason
     ) : AuthenticationState
 
@@ -131,7 +138,7 @@
      * @param userId The user id for the requested authentication
      */
     data class Succeeded(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val isStrongBiometric: Boolean,
         override val requestReason: AuthenticationReason,
         val userId: Int
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7f236f7..3ef5572 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -67,6 +67,7 @@
 import kotlinx.coroutines.launch
 
 private const val TAG = "BiometricViewBinder"
+private const val MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30
 
 /** Top-most view binder for BiometricPrompt views. */
 object BiometricViewBinder {
@@ -195,8 +196,12 @@
                 }
             }
 
-            logoView.setImageDrawable(viewModel.logo.first())
-            logoDescriptionView.text = viewModel.logoDescription.first()
+            val logoInfo = viewModel.logoInfo.first()
+            logoView.setImageDrawable(logoInfo.first)
+            // The ellipsize effect on xml happens only when the TextView does not have any free
+            // space on the screen to show the text. So we need to manually truncate.
+            logoDescriptionView.text =
+                logoInfo.second?.ellipsize(MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER)
             titleView.text = viewModel.title.first()
             subtitleView.text = viewModel.subtitle.first()
             descriptionView.text = viewModel.description.first()
@@ -717,6 +722,9 @@
         else -> ""
     }
 
+private fun String.ellipsize(cutOffLength: Int) =
+    if (length <= cutOffLength) this else replaceRange(cutOffLength, length, "...")
+
 private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE
 
 private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index b1cba2f..d1ac681 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -36,7 +36,6 @@
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import com.android.launcher3.icons.IconProvider
-import com.android.systemui.Flags.bpTalkback
 import com.android.systemui.Flags.constraintBp
 import com.android.systemui.biometrics.UdfpsUtils
 import com.android.systemui.biometrics.Utils
@@ -471,26 +470,13 @@
             }
         }
 
-    /** Logo for the prompt. */
-    val logo: Flow<Drawable?> =
+    /** (logoIcon, logoDescription) for the prompt. */
+    val logoInfo: Flow<Pair<Drawable?, String>> =
         promptSelectorInteractor.prompt
             .map {
                 when {
-                    !(customBiometricPrompt() && constraintBp()) || it == null -> null
-                    it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap)
-                    else -> context.getUserBadgedIcon(it, iconProvider, activityTaskManager)
-                }
-            }
-            .distinctUntilChanged()
-
-    /** Logo description for the prompt. */
-    val logoDescription: Flow<String> =
-        promptSelectorInteractor.prompt
-            .map {
-                when {
-                    !(customBiometricPrompt() && constraintBp()) || it == null -> ""
-                    !it.logoDescription.isNullOrEmpty() -> it.logoDescription
-                    else -> context.getUserBadgedLabel(it, activityTaskManager)
+                    !(customBiometricPrompt() && constraintBp()) || it == null -> Pair(null, "")
+                    else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager)
                 }
             }
             .distinctUntilChanged()
@@ -916,10 +902,9 @@
         touchExplorationEnabled: Boolean,
     ): Boolean {
         if (
-            bpTalkback() &&
-                modalities.first().hasUdfps &&
-                touchExplorationEnabled &&
-                !isAuthenticated.first().isAuthenticated
+            modalities.first().hasUdfps &&
+            touchExplorationEnabled &&
+            !isAuthenticated.first().isAuthenticated
         ) {
             // TODO(b/315184924): Remove uses of UdfpsUtils
             val scaledTouch =
@@ -987,43 +972,60 @@
     }
 }
 
-private fun Context.getUserBadgedIcon(
+/**
+ * The order of getting logo icon/description is:
+ * 1. If the app sets customized icon/description, use the passed-in value
+ * 2. If shouldShowLogoWithOverrides(), use activityInfo to get icon/description
+ * 3. Otherwise, use applicationInfo to get icon/description
+ */
+private fun Context.getUserBadgedLogoInfo(
     prompt: BiometricPromptRequest.Biometric,
     iconProvider: IconProvider,
     activityTaskManager: ActivityTaskManager
-): Drawable? {
-    var icon: Drawable? = null
-    val componentName = prompt.getComponentNameForLogo(activityTaskManager)
-    if (componentName != null && shouldShowLogoWithOverrides(componentName)) {
-        val activityInfo = getActivityInfo(componentName)
-        icon = if (activityInfo == null) null else iconProvider.getIcon(activityInfo)
+): Pair<Drawable?, String> {
+    var icon: Drawable? =
+        if (prompt.logoBitmap != null) BitmapDrawable(resources, prompt.logoBitmap) else null
+    var label = prompt.logoDescription ?: ""
+    if (icon != null && label.isNotEmpty()) {
+        return Pair(icon, label)
     }
-    if (icon == null) {
-        val appInfo = prompt.getApplicationInfoForLogo(this, componentName)
-        if (appInfo == null) {
-            Log.w(PromptViewModel.TAG, "Cannot find app logo for package $opPackageName")
-            return null
-        } else {
-            icon = packageManager.getApplicationIcon(appInfo)
+
+    // Use activityInfo if shouldUseActivityLogo() is true
+    val componentName = prompt.getComponentNameForLogo(activityTaskManager)
+    if (componentName != null && shouldUseActivityLogo(componentName)) {
+        val activityInfo = getActivityInfo(componentName)
+        if (activityInfo != null) {
+            if (icon == null) {
+                icon = iconProvider.getIcon(activityInfo)
+            }
+            if (label.isEmpty()) {
+                label = activityInfo.loadLabel(packageManager).toString()
+            }
         }
     }
-    return packageManager.getUserBadgedIcon(icon, UserHandle.of(prompt.userInfo.userId))
-}
-
-private fun Context.getUserBadgedLabel(
-    prompt: BiometricPromptRequest.Biometric,
-    activityTaskManager: ActivityTaskManager
-): String {
-    val componentName = prompt.getComponentNameForLogo(activityTaskManager)
-    val appInfo = prompt.getApplicationInfoForLogo(this, componentName)
-    return if (appInfo == null || packageManager.getApplicationLabel(appInfo).isNullOrEmpty()) {
-        Log.w(PromptViewModel.TAG, "Cannot find app logo for package $opPackageName")
-        ""
-    } else {
-        packageManager
-            .getUserBadgedLabel(packageManager.getApplicationLabel(appInfo), UserHandle.of(userId))
-            .toString()
+    if (icon != null && label.isNotEmpty()) {
+        return Pair(icon, label)
     }
+
+    // Use applicationInfo for other cases
+    val appInfo = prompt.getApplicationInfo(this, componentName)
+    if (appInfo == null) {
+        Log.w(PromptViewModel.TAG, "Cannot find app logo for package $opPackageName")
+    } else {
+        if (icon == null) {
+            icon = packageManager.getApplicationIcon(appInfo)
+        }
+        if (label.isEmpty()) {
+            label =
+                packageManager
+                    .getUserBadgedLabel(
+                        packageManager.getApplicationLabel(appInfo),
+                        UserHandle.of(userId)
+                    )
+                    .toString()
+        }
+    }
+    return Pair(icon, label)
 }
 
 private fun BiometricPromptRequest.Biometric.getComponentNameForLogo(
@@ -1041,7 +1043,7 @@
     }
 }
 
-private fun BiometricPromptRequest.Biometric.getApplicationInfoForLogo(
+private fun BiometricPromptRequest.Biometric.getApplicationInfo(
     context: Context,
     componentNameForLogo: ComponentName?
 ): ApplicationInfo? {
@@ -1058,14 +1060,22 @@
         Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName")
         null
     } else {
-        context.getApplicationInfo(packageName)
+        try {
+            context.packageManager.getApplicationInfo(
+                packageName,
+                PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
+            )
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName", e)
+            null
+        }
     }
 }
 
-private fun Context.shouldShowLogoWithOverrides(componentName: ComponentName): Boolean {
-    return resources
-        .getStringArray(R.array.biometric_dialog_package_names_for_logo_with_overrides)
-        .find { componentName.packageName.contentEquals(it) } != null
+private fun Context.shouldUseActivityLogo(componentName: ComponentName): Boolean {
+    return resources.getStringArray(R.array.config_useActivityLogoForBiometricPrompt).find {
+        componentName.packageName.contentEquals(it)
+    } != null
 }
 
 private fun Context.getActivityInfo(componentName: ComponentName): ActivityInfo? =
@@ -1076,17 +1086,6 @@
         null
     }
 
-private fun Context.getApplicationInfo(packageName: String): ApplicationInfo? =
-    try {
-        packageManager.getApplicationInfo(
-            packageName,
-            PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
-        )
-    } catch (e: PackageManager.NameNotFoundException) {
-        Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName", e)
-        null
-    }
-
 /** How the fingerprint sensor was started for the prompt. */
 enum class FingerprintStartMode {
     /** Fingerprint sensor has not started. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
index 64dedea..108e22b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.common.ui.binder
 
-import android.view.View
 import android.widget.ImageView
 import com.android.systemui.common.shared.model.Icon
 
@@ -31,13 +30,4 @@
             is Icon.Resource -> view.setImageResource(icon.res)
         }
     }
-
-    fun bindNullable(icon: Icon?, view: ImageView) {
-        if (icon != null) {
-            view.visibility = View.VISIBLE
-            bind(icon, view)
-        } else {
-            view.visibility = View.GONE
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 6a20610..c780aac 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -175,7 +175,7 @@
         }
     }
 
-    private fun finishCurrentTransition() {
+    private suspend fun finishCurrentTransition() {
         internalTransitionInteractor.updateTransition(
             currentTransitionId!!,
             1f,
@@ -285,7 +285,7 @@
         currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo)
     }
 
-    private fun updateProgress(progress: Float) {
+    private suspend fun updateProgress(progress: Float) {
         if (currentTransitionId == null) return
         internalTransitionInteractor.updateTransition(
             currentTransitionId!!,
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 3fc8b09..b06cf3f 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
@@ -97,8 +97,10 @@
     private val metricsLogger: CommunalMetricsLogger,
 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
 
+    private val logger = Logger(logBuffer, "CommunalViewModel")
+
     private val _isMediaHostVisible =
-        conflatedCallbackFlow<Boolean> {
+        conflatedCallbackFlow {
                 val callback = { visible: Boolean ->
                     trySend(visible)
                     Unit
@@ -106,11 +108,18 @@
                 mediaHost.addVisibilityChangeListener(callback)
                 awaitClose { mediaHost.removeVisibilityChangeListener(callback) }
             }
-            .onStart { emit(mediaHost.visible) }
+            .onStart {
+                // Ensure the visibility state is correct when the hub is opened and this flow is
+                // started so that the UMO is shown when needed. The visibility state in MediaHost
+                // is not updated once its view has been detached, aka the hub is closed, which can
+                // result in this getting stuck as False and never being updated as the UMO is not
+                // shown.
+                mediaHost.updateViewVisibility()
+                emit(mediaHost.visible)
+            }
+            .onEach { logger.d({ "_isMediaHostVisible: $bool1" }) { bool1 = it } }
             .flowOn(mainDispatcher)
 
-    private val logger = Logger(logBuffer, "CommunalViewModel")
-
     /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
     private var frozenCommunalContent: List<CommunalContentModel>? = null
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 058ca4d..10a565f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -36,7 +36,7 @@
     context: Context,
     private val backgroundScope: CoroutineScope,
     hostId: Int,
-    interactionHandler: RemoteViews.InteractionHandler,
+    private val interactionHandler: RemoteViews.InteractionHandler,
     looper: Looper,
     logBuffer: LogBuffer,
 ) : AppWidgetHost(context, hostId, interactionHandler, looper) {
@@ -55,7 +55,7 @@
         appWidgetId: Int,
         appWidget: AppWidgetProviderInfo?
     ): AppWidgetHostView {
-        return CommunalAppWidgetHostView(context)
+        return CommunalAppWidgetHostView(context, interactionHandler)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
index 2559137..d549734 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
@@ -17,17 +17,25 @@
 package com.android.systemui.communal.widgets
 
 import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.content.Context
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
 import android.graphics.Outline
 import android.graphics.Rect
 import android.view.View
 import android.view.ViewOutlineProvider
+import android.widget.RemoteViews
+import android.widget.RemoteViews.RemoteResponse
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 
 /** AppWidgetHostView that displays in communal hub with support for rounded corners. */
-class CommunalAppWidgetHostView(context: Context) : AppWidgetHostView(context), LaunchableView {
+class CommunalAppWidgetHostView(
+    context: Context,
+    private val interactionHandler: RemoteViews.InteractionHandler,
+) : AppWidgetHostView(context, interactionHandler), LaunchableView {
     private val launchableViewDelegate =
         LaunchableViewDelegate(
             this,
@@ -92,4 +100,26 @@
         launchableViewDelegate.setShouldBlockVisibilityChanges(block)
 
     override fun setVisibility(visibility: Int) = launchableViewDelegate.setVisibility(visibility)
+
+    override fun onDefaultViewClicked(view: View) {
+        AppWidgetManager.getInstance(context)?.noteAppWidgetTapped(appWidgetId)
+        if (appWidgetInfo == null) {
+            return
+        }
+        val launcherApps = context.getSystemService(LauncherApps::class.java)
+        val activityInfo: LauncherActivityInfo =
+            launcherApps
+                .getActivityList(appWidgetInfo.provider.packageName, appWidgetInfo.profile)
+                ?.getOrNull(0) ?: return
+
+        val intent =
+            launcherApps.getMainActivityLaunchIntent(
+                activityInfo.componentName,
+                null,
+                activityInfo.user
+            )
+        if (intent != null) {
+            interactionHandler.onInteraction(view, intent, RemoteResponse.fromPendingIntent(intent))
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 6ed84e5..89fce4a 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -293,11 +293,11 @@
         if (shouldUseFloatBrightness()) {
             mDozeService.setDozeScreenBrightnessFloat(
                     clampToDimBrightnessForScreenOffFloat(
-                            clampToUserSettingFloat(mDefaultDozeBrightnessFloat)));
+                            clampToUserSettingOrAutoBrightnessFloat(mDefaultDozeBrightnessFloat)));
         } else {
             mDozeService.setDozeScreenBrightness(
                     clampToDimBrightnessForScreenOff(
-                            clampToUserSetting(mDefaultDozeBrightness)));
+                            clampToUserSettingOrAutoBrightness(mDefaultDozeBrightness)));
         }
         mDozeHost.setAodDimmingScrim(0f);
     }
@@ -310,10 +310,7 @@
             return brightness;
         }
 
-        int userSetting = mSystemSettings.getIntForUser(
-                Settings.System.SCREEN_BRIGHTNESS, Integer.MAX_VALUE,
-                UserHandle.USER_CURRENT);
-        return Math.min(brightness, userSetting);
+        return Math.min(brightness, getScreenBrightness());
     }
 
     @SuppressLint("AndroidFrameworkRequiresPermission")
@@ -325,8 +322,33 @@
             return brightness;
         }
 
-        float userSetting = mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY);
-        return Math.min(brightness, userSetting);
+        return Math.min(brightness, getScreenBrightnessFloat());
+    }
+
+    private int clampToUserSettingOrAutoBrightness(int brightness) {
+        return Math.min(brightness, getScreenBrightness());
+    }
+
+    private float clampToUserSettingOrAutoBrightnessFloat(float brightness) {
+        return Math.min(brightness, getScreenBrightnessFloat());
+    }
+
+    /**
+     * Gets the current screen brightness that may have been set by manually by the user
+     * or by autobrightness.
+     */
+    private int getScreenBrightness() {
+        return mSystemSettings.getIntForUser(
+                Settings.System.SCREEN_BRIGHTNESS, Integer.MAX_VALUE,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
+     * Gets the current screen brightness that may have been set by manually by the user
+     * or by autobrightness.
+     */
+    private float getScreenBrightnessFloat() {
+        return mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index b8019ab..532b123 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -19,13 +19,13 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.education.data.repository.ContextualEducationRepository
 import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
 import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
-import com.android.systemui.shared.education.GestureType
 import dagger.Binds
 import dagger.Lazy
 import dagger.Module
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt
index 248b7a5..52ccba4 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
 import com.android.systemui.education.data.model.GestureEduModel
-import com.android.systemui.shared.education.GestureType
 import java.time.Clock
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index b7fc773..4b37b29 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -27,9 +27,9 @@
 import androidx.datastore.preferences.preferencesDataStoreFile
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
 import com.android.systemui.education.data.model.GestureEduModel
-import com.android.systemui.shared.education.GestureType
 import java.time.Instant
 import javax.inject.Inject
 import javax.inject.Provider
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index 3036d97..bee289d 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -19,9 +19,10 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.education.data.model.GestureEduModel
 import com.android.systemui.education.data.repository.ContextualEducationRepository
-import com.android.systemui.shared.education.GestureType
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -46,7 +47,7 @@
     private val repository: ContextualEducationRepository,
 ) : CoreStartable {
 
-    val backGestureModelFlow = readEduModelsOnSignalCountChanged(GestureType.BACK_GESTURE)
+    val backGestureModelFlow = readEduModelsOnSignalCountChanged(BACK)
 
     override fun start() {
         backgroundScope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 247abf1..9016c73 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -19,10 +19,10 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.education.data.model.GestureEduModel
 import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
-import com.android.systemui.shared.education.GestureType.BACK_GESTURE
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -50,7 +50,7 @@
         backgroundScope.launch {
             contextualEducationInteractor.backGestureModelFlow
                 .mapNotNull { getEduType(it) }
-                .collect { _educationTriggered.value = EducationInfo(BACK_GESTURE, it) }
+                .collect { _educationTriggered.value = EducationInfo(BACK, it) }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
index 643e571..3223433 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.shared.education.GestureType
+import com.android.systemui.contextualeducation.GestureType
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
diff --git a/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt
index 85f4012..d92fb9b 100644
--- a/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.education.shared.model
 
-import com.android.systemui.shared.education.GestureType
+import com.android.systemui.contextualeducation.GestureType
 
 /**
  * Model for education triggered. [gestureType] indicates what gesture it is trying to educate about
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 6e4038d..59de203 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,7 +23,13 @@
 import com.android.server.notification.Flags.politeNotifications
 import com.android.server.notification.Flags.vibrateWhileUnlocked
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
+import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
 import com.android.systemui.Flags.communalHub
+import com.android.systemui.Flags.statusBarCallChipNotificationIcon
+import com.android.systemui.Flags.statusBarScreenSharingChips
+import com.android.systemui.Flags.statusBarUseReposForCallChip
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
@@ -38,9 +44,9 @@
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
 
@@ -77,6 +83,10 @@
 
         // QS Fragment using Compose dependencies
         QSComposeFragment.token dependsOn NewQsUI.token
+
+        // Status bar chip dependencies
+        statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
+        statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
     }
 
     private inline val politeNotifications
@@ -96,4 +106,17 @@
 
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
+
+    private inline val statusBarCallChipNotificationIconToken
+        get() =
+            FlagToken(
+                FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON,
+                statusBarCallChipNotificationIcon()
+            )
+
+    private inline val statusBarScreenSharingChipsToken
+        get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips())
+
+    private inline val statusBarUseReposForCallChipToken
+        get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index aaeeb39..de60c11 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -105,7 +105,7 @@
      * When the transition is over, TransitionState.FINISHED must be passed into the [state]
      * parameter.
      */
-    fun updateTransition(
+    suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState
@@ -173,9 +173,9 @@
         Log.d(TAG, "(Internal) Setting current transition info: $info")
 
         // There is no fairness guarantee with 'withContext', which means that transitions could
-        // be processed out of order. Use a Mutex to guarantee ordering.
+        // be processed out of order. Use a Mutex to guarantee ordering. [updateTransition]
+        // requires the same lock
         _currentTransitionMutex.lock()
-
         // Only used in a test environment
         if (forceDelayForRaceConditionTest) {
             delay(50L)
@@ -184,7 +184,6 @@
         // Animators must be started on the main thread.
         return withContext("$TAG#startTransition", mainDispatcher) {
             _currentTransitionMutex.unlock()
-
             if (lastStep.from == info.from && lastStep.to == info.to) {
                 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
                 return@withContext null
@@ -206,7 +205,7 @@
 
             // Cancel any existing manual transitions
             updateTransitionId?.let { uuid ->
-                updateTransition(uuid, lastStep.value, TransitionState.CANCELED)
+                updateTransitionInternal(uuid, lastStep.value, TransitionState.CANCELED)
             }
 
             info.animator?.let { animator ->
@@ -264,7 +263,23 @@
         }
     }
 
-    override fun updateTransition(
+    override suspend fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) {
+        // There is no fairness guarantee with 'withContext', which means that transitions could
+        // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
+        // requires the same lock
+        _currentTransitionMutex.lock()
+        withContext("$TAG#updateTransition", mainDispatcher) {
+            _currentTransitionMutex.unlock()
+
+            updateTransitionInternal(transitionId, value, state)
+        }
+    }
+
+    private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 91ee287..befcc9e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -96,6 +96,8 @@
                 when (toState) {
                     KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
                     KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+                    KeyguardState.ALTERNATE_BOUNCER -> TO_BOUNCER_DURATION
+                    KeyguardState.PRIMARY_BOUNCER -> TO_BOUNCER_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -269,8 +271,16 @@
 
     companion object {
         const val TAG = "FromGlanceableHubTransitionInteractor"
-        val DEFAULT_DURATION = 1.seconds
-        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
+
+        /**
+         * DEFAULT_DURATION controls the timing for all animations other than those with overrides
+         * in [getDefaultAnimatorForTransitionsToState].
+         *
+         * Set at 400ms for parity with [FromLockscreenTransitionInteractor]
+         */
+        val DEFAULT_DURATION = 400.milliseconds
+        val TO_LOCKSCREEN_DURATION = 1.seconds
+        val TO_BOUNCER_DURATION = 400.milliseconds
         val TO_OCCLUDED_DURATION = 450.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
index a51421c..2cc6afa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
@@ -63,7 +63,7 @@
 
     suspend fun startTransition(info: TransitionInfo) = repository.startTransition(info)
 
-    fun updateTransition(
+    suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 797d466..afbe357 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -47,6 +47,7 @@
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.buffer
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
@@ -55,6 +56,7 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
 import kotlinx.coroutines.launch
 
 /** Encapsulates business-logic related to the keyguard transitions. */
@@ -150,6 +152,12 @@
                         startedStep.to != prevStep.from
                 ) {
                     getTransitionValueFlow(prevStep.from).emit(0f)
+                } else if (prevStep.transitionState == TransitionState.RUNNING) {
+                    Log.e(
+                        TAG,
+                        "STARTED step ($startedStep) was preceded by a RUNNING step " +
+                            "($prevStep), which should never happen. Things could go badly here."
+                    )
                 }
             }
         }
@@ -252,15 +260,12 @@
     val startedKeyguardTransitionStep: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
 
-    /** The last [TransitionStep] with a [TransitionState] of FINISHED */
-    val finishedKeyguardTransitionStep: Flow<TransitionStep> =
-        repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
-
     /** The destination state of the last [TransitionState.STARTED] transition. */
     @SuppressLint("SharedFlowCreation")
     val startedKeyguardState: SharedFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.to }
+            .buffer(2, BufferOverflow.DROP_OLDEST)
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /** The from state of the last [TransitionState.STARTED] transition. */
@@ -269,6 +274,7 @@
     val startedKeyguardFromState: SharedFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.from }
+            .buffer(2, BufferOverflow.DROP_OLDEST)
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /** Which keyguard state to use when the device goes to sleep. */
@@ -310,8 +316,13 @@
      */
     @SuppressLint("SharedFlowCreation")
     val finishedKeyguardState: SharedFlow<KeyguardState> =
-        finishedKeyguardTransitionStep
-            .map { step -> step.to }
+        repository.transitions
+            .transform { step ->
+                if (step.transitionState == TransitionState.FINISHED) {
+                    emit(step.to)
+                }
+            }
+            .buffer(2, BufferOverflow.DROP_OLDEST)
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /**
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 3248114..b850095 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
@@ -124,7 +124,7 @@
         }
     }
 
-    private fun finishCurrentTransition() {
+    private suspend fun finishCurrentTransition() {
         internalTransitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
         resetTransitionData()
     }
@@ -223,7 +223,7 @@
         currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo)
     }
 
-    private fun updateProgress(progress: Float) {
+    private suspend fun updateProgress(progress: Float) {
         if (currentTransitionId == null) return
         internalTransitionInteractor.updateTransition(
             currentTransitionId!!,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 23aa21c..0cb8dd4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -177,7 +177,12 @@
          * Immediately (after 1ms) emits the given value for every step of the KeyguardTransition.
          */
         fun immediatelyTransitionTo(value: Float): Flow<Float> {
-            return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
+            return sharedFlow(
+                duration = 1.milliseconds,
+                onStep = { value },
+                onCancel = { value },
+                onFinish = { value }
+            )
         }
     }
 }
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 f96f053..91b66c3 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
@@ -207,14 +207,12 @@
                         launch {
                             viewModel.burnInLayerVisibility.collect { visibility ->
                                 childViews[burnInLayerId]?.visibility = visibility
-                                childViews[aodNotificationIconContainerId]?.visibility = visibility
                             }
                         }
 
                         launch {
                             viewModel.burnInLayerAlpha.collect { alpha ->
                                 childViews[statusViewId]?.alpha = alpha
-                                childViews[aodNotificationIconContainerId]?.alpha = alpha
                             }
                         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
index 9da11ce..c590f07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -51,6 +51,7 @@
         transitionAnimation.sharedFlow(
             duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
             onStep = { 1 - it },
+            onCancel = { 0f },
             onFinish = { 0f },
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index b267ecb..6b22c0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -104,12 +104,10 @@
         transitionAnimation.sharedFlow(
             duration = 250.milliseconds,
             onStep = { it },
+            onCancel = { 1f },
             onFinish = { 1f },
         )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 500.milliseconds,
-            onStep = { 1f },
-        )
+        transitionAnimation.immediatelyTransitionTo(1f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
index 1ee0368..7562392 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
@@ -63,6 +63,7 @@
                 transitionAnimation.sharedFlow(
                     duration = FromDreamingTransitionInteractor.TO_AOD_DURATION,
                     onStep = { it },
+                    onCancel = { 1f },
                     onFinish = { 1f },
                 )
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
index ea8fe29..11ed52a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
@@ -95,7 +95,7 @@
             startTime = 167.milliseconds,
             duration = 167.milliseconds,
             onStep = { it },
-            onCancel = { 0f },
+            onCancel = { 1f },
             onFinish = { 1f },
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 82381eb..b5ec7a6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -95,5 +95,11 @@
         )
 
     val deviceEntryBackgroundViewAlpha = transitionAnimation.immediatelyTransitionTo(1f)
-    override val deviceEntryParentViewAlpha = lockscreenAlpha
+    override val deviceEntryParentViewAlpha =
+        transitionAnimation.sharedFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onCancel = { 1f },
+            onStep = { it },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt
index 76d5a8d..f69f996 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt
@@ -93,7 +93,7 @@
         transitionAnimation.sharedFlow(
             duration = 167.milliseconds,
             onStep = { 1 - it },
-            onCancel = { 1f },
+            onCancel = { 0f },
             onFinish = { 0f },
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index 2bc8e51..43872b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -122,6 +122,7 @@
                     startTime = 1100.milliseconds,
                     duration = 200.milliseconds,
                     onStep = { it },
+                    onCancel = { 1f },
                     onFinish = { 1f },
                 )
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
index 6a3573a..6d95ade 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
@@ -43,7 +43,7 @@
         transitionAnimation.sharedFlow(
             duration = 167.milliseconds,
             onStep = { it },
-            onCancel = { 0f },
+            onCancel = { 1f },
             onFinish = { 1f },
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 11889c5..38a2b1b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -320,19 +320,24 @@
     val isNotifIconContainerVisible: StateFlow<AnimatedValue<Boolean>> =
         combine(
                 goneToAodTransitionRunning,
+                keyguardTransitionInteractor
+                    .transitionValue(LOCKSCREEN)
+                    .map { it > 0f }
+                    .onStart { emit(false) },
                 keyguardTransitionInteractor.finishedKeyguardState.map {
                     KeyguardState.lockscreenVisibleInState(it)
                 },
                 deviceEntryInteractor.isBypassEnabled,
                 areNotifsFullyHiddenAnimated(),
                 isPulseExpandingAnimated(),
-            ) {
-                goneToAodTransitionRunning: Boolean,
-                onKeyguard: Boolean,
-                isBypassEnabled: Boolean,
-                notifsFullyHidden: AnimatedValue<Boolean>,
-                pulseExpanding: AnimatedValue<Boolean>,
-                ->
+            ) { flows ->
+                val goneToAodTransitionRunning = flows[0] as Boolean
+                val isOnLockscreen = flows[1] as Boolean
+                val onKeyguard = flows[2] as Boolean
+                val isBypassEnabled = flows[3] as Boolean
+                val notifsFullyHidden = flows[4] as AnimatedValue<Boolean>
+                val pulseExpanding = flows[5] as AnimatedValue<Boolean>
+
                 when {
                     // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off
                     // animation is playing, in which case we want them to be visible if we're
@@ -351,6 +356,8 @@
                                 isBypassEnabled -> true
                                 // If we are pulsing (and not bypassing), then we are hidden
                                 isPulseExpanding -> false
+                                // Besides bypass above, they should not be visible on lockscreen
+                                isOnLockscreen -> false
                                 // If notifs are fully gone, then we're visible
                                 areNotifsFullyHidden -> true
                                 // Otherwise, we're hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
index 5408428..2f21ebc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
@@ -70,6 +70,7 @@
                 transitionAnimation.sharedFlow(
                     duration = 300.milliseconds,
                     onStep = { 1 - it },
+                    onCancel = { 0f },
                     onFinish = { 0f },
                 ),
         )
@@ -153,6 +154,7 @@
                     transitionAnimation.sharedFlow(
                             duration = 300.milliseconds,
                             onStep = { it },
+                            onCancel = { 1f },
                             onFinish = { 1f },
                         ),
                     flowWhenShadeIsNotExpanded = transitionAnimation.immediatelyTransitionTo(1f),
@@ -164,6 +166,7 @@
                     transitionAnimation.sharedFlow(
                             duration = 200.milliseconds,
                             onStep = { 1f - it },
+                            onCancel = { 0f },
                             onFinish = { 0f },
                         ),
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index 579abeb..bbb55cc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -74,7 +74,12 @@
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         shadeDependentFlows.transitionFlow(
-            flowWhenShadeIsNotExpanded = lockscreenAlpha,
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onCancel = { 0f },
+                ),
             flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index fcf8c14f..8d9ccef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -83,7 +83,12 @@
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         shadeDependentFlows.transitionFlow(
-            flowWhenShadeIsNotExpanded = lockscreenAlpha,
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onCancel = { 0f },
+                ),
             flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 23c44b0a..e64c614 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -65,6 +65,7 @@
                 transitionAnimation.sharedFlow(
                     duration = 250.milliseconds,
                     onStep = { 1f - it },
+                    onCancel = { 0f },
                     onFinish = { 0f }
                 ),
             flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index 36c7d5b..737bd7a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -110,5 +110,11 @@
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
 
-    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
+            onCancel = { 1f },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index 009f85d..501feca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -70,6 +70,7 @@
                 transitionAnimation.sharedFlow(
                     duration = 300.milliseconds,
                     onStep = { it },
+                    onCancel = { 1f },
                     onFinish = { 1f },
                 )
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionUtils.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionUtils.kt
new file mode 100644
index 0000000..723ff5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.mediaprojection
+
+import android.content.pm.PackageManager
+import com.android.systemui.util.Utils
+
+/** Various utility methods related to media projection. */
+object MediaProjectionUtils {
+    /**
+     * Returns true iff projecting to the given [packageName] means that we're casting media to a
+     * *different* device (as opposed to sharing media to some application on *this* device).
+     */
+    fun packageHasCastingCapabilities(
+        packageManager: PackageManager,
+        packageName: String
+    ): Boolean {
+        // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
+        // different device or the same device, because headless remote display packages are the
+        // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
+        // because it means that any projection by those headless remote display packages will be
+        // marked as going to a different device, even if that isn't always true. See b/321078669.
+        return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index cc4a92c..3c83db3 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -41,7 +41,6 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.graphics.Typeface;
 import android.media.projection.IMediaProjection;
 import android.media.projection.MediaProjectionConfig;
 import android.media.projection.MediaProjectionManager;
@@ -50,10 +49,8 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.text.BidiFormatter;
-import android.text.SpannableString;
 import android.text.TextPaint;
 import android.text.TextUtils;
-import android.text.style.StyleSpan;
 import android.util.Log;
 import android.view.Window;
 
@@ -61,6 +58,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper;
+import com.android.systemui.mediaprojection.MediaProjectionUtils;
 import com.android.systemui.mediaprojection.SessionCreationSource;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
@@ -68,12 +66,13 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.util.Utils;
 
-import dagger.Lazy;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 public class MediaProjectionPermissionActivity extends Activity
         implements DialogInterface.OnClickListener {
     private static final String TAG = "MediaProjectionPermissionActivity";
@@ -189,30 +188,14 @@
 
         final String appName = extractAppName(aInfo, packageManager);
         final boolean hasCastingCapabilities =
-                Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName);
+                MediaProjectionUtils.INSTANCE.packageHasCastingCapabilities(
+                        packageManager, mPackageName);
 
         // Using application context for the dialog, instead of the activity context, so we get
         // the correct screen width when in split screen.
         Context dialogContext = getApplicationContext();
-        final boolean overrideDisableSingleAppOption =
-                CompatChanges.isChangeEnabled(
-                        OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
-                        mPackageName, getHostUserHandle());
-        MediaProjectionPermissionDialogDelegate delegate =
-                new MediaProjectionPermissionDialogDelegate(
-                        dialogContext,
-                        getMediaProjectionConfig(),
-                        dialog -> {
-                            ScreenShareOption selectedOption =
-                                    dialog.getSelectedScreenShareOption();
-                            grantMediaProjectionPermission(selectedOption.getMode());
-                        },
-                        () -> finish(RECORD_CANCEL, /* projection= */ null),
-                        hasCastingCapabilities,
-                        appName,
-                        overrideDisableSingleAppOption,
-                        mUid,
-                        mMediaProjectionMetricsLogger);
+        BaseMediaProjectionPermissionDialogDelegate<AlertDialog> delegate =
+                createPermissionDialogDelegate(appName, hasCastingCapabilities, dialogContext);
         mDialog =
                 new AlertDialogWithDelegate(
                         dialogContext, R.style.Theme_SystemUI_Dialog, delegate);
@@ -274,6 +257,44 @@
         return appName;
     }
 
+    private BaseMediaProjectionPermissionDialogDelegate<AlertDialog> createPermissionDialogDelegate(
+            String appName,
+            boolean hasCastingCapabilities,
+            Context dialogContext) {
+        final boolean overrideDisableSingleAppOption =
+                CompatChanges.isChangeEnabled(
+                        OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
+                        mPackageName, getHostUserHandle());
+        MediaProjectionConfig mediaProjectionConfig = getMediaProjectionConfig();
+        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>> onStartRecordingClicked =
+                dialog -> {
+                    ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption();
+                    grantMediaProjectionPermission(selectedOption.getMode());
+                };
+        Runnable onCancelClicked = () -> finish(RECORD_CANCEL, /* projection= */ null);
+        if (hasCastingCapabilities) {
+            return new SystemCastPermissionDialogDelegate(
+                    dialogContext,
+                    mediaProjectionConfig,
+                    onStartRecordingClicked,
+                    onCancelClicked,
+                    appName,
+                    overrideDisableSingleAppOption,
+                    mUid,
+                    mMediaProjectionMetricsLogger);
+        } else {
+            return new ShareToAppPermissionDialogDelegate(
+                    dialogContext,
+                    mediaProjectionConfig,
+                    onStartRecordingClicked,
+                    onCancelClicked,
+                    appName,
+                    overrideDisableSingleAppOption,
+                    mUid,
+                    mMediaProjectionMetricsLogger);
+        }
+    }
+
     @Override
     protected void onDestroy() {
         super.onDestroy();
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
deleted file mode 100644
index 6d1a458..0000000
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.
- */
-package com.android.systemui.mediaprojection.permission
-
-import android.app.AlertDialog
-import android.content.Context
-import android.media.projection.MediaProjectionConfig
-import android.os.Bundle
-import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
-import com.android.systemui.res.R
-import java.util.function.Consumer
-
-/** Dialog to select screen recording options */
-class MediaProjectionPermissionDialogDelegate(
-    context: Context,
-    mediaProjectionConfig: MediaProjectionConfig?,
-    private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
-    private val onCancelClicked: Runnable,
-    private val hasCastingCapabilities: Boolean,
-    appName: String,
-    forceShowPartialScreenshare: Boolean,
-    hostUid: Int,
-    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
-) :
-    BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
-        createOptionList(
-            context,
-            appName,
-            hasCastingCapabilities,
-            mediaProjectionConfig,
-            forceShowPartialScreenshare
-        ),
-        appName,
-        hostUid,
-        mediaProjectionMetricsLogger
-    ) {
-    override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
-        super.onCreate(dialog, savedInstanceState)
-        // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
-        if (hasCastingCapabilities) {
-            setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
-            setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
-        } else {
-            setDialogTitle(R.string.media_projection_entry_app_permission_dialog_title)
-            setStartButtonText(R.string.media_projection_entry_app_permission_dialog_continue)
-        }
-        setStartButtonOnClickListener {
-            // Note that it is important to run this callback before dismissing, so that the
-            // callback can disable the dialog exit animation if it wants to.
-            onStartRecordingClicked.accept(this)
-            dialog.dismiss()
-        }
-        setCancelButtonOnClickListener {
-            onCancelClicked.run()
-            dialog.dismiss()
-        }
-    }
-
-    companion object {
-        private fun createOptionList(
-            context: Context,
-            appName: String,
-            hasCastingCapabilities: Boolean,
-            mediaProjectionConfig: MediaProjectionConfig?,
-            overrideDisableSingleAppOption: Boolean = false,
-        ): List<ScreenShareOption> {
-            val singleAppWarningText =
-                if (hasCastingCapabilities) {
-                    R.string.media_projection_entry_cast_permission_dialog_warning_single_app
-                } else {
-                    R.string.media_projection_entry_app_permission_dialog_warning_single_app
-                }
-            val entireScreenWarningText =
-                if (hasCastingCapabilities) {
-                    R.string.media_projection_entry_cast_permission_dialog_warning_entire_screen
-                } else {
-                    R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
-                }
-
-            // The single app option should only be disabled if the client has setup a
-            // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
-            // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
-            val singleAppOptionDisabled =
-                !overrideDisableSingleAppOption &&
-                    mediaProjectionConfig?.regionToCapture ==
-                        MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
-
-            val singleAppDisabledText =
-                if (singleAppOptionDisabled) {
-                    context.getString(
-                        R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
-                        appName
-                    )
-                } else {
-                    null
-                }
-            val options =
-                listOf(
-                    ScreenShareOption(
-                        mode = SINGLE_APP,
-                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
-                        warningText = singleAppWarningText,
-                        spinnerDisabledText = singleAppDisabledText,
-                    ),
-                    ScreenShareOption(
-                        mode = ENTIRE_SCREEN,
-                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
-                        warningText = entireScreenWarningText
-                    )
-                )
-            return if (singleAppOptionDisabled) {
-                // Make sure "Entire screen" is the first option when "Single App" is disabled.
-                options.reversed()
-            } else {
-                options
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt
new file mode 100644
index 0000000..88cbc38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.mediaprojection.permission
+
+import android.content.Context
+import android.media.projection.MediaProjectionConfig
+import com.android.systemui.res.R
+
+/** Various utility methods related to media projection permissions. */
+object MediaProjectionPermissionUtils {
+    fun getSingleAppDisabledText(
+        context: Context,
+        appName: String,
+        mediaProjectionConfig: MediaProjectionConfig?,
+        overrideDisableSingleAppOption: Boolean,
+    ): String? {
+        // The single app option should only be disabled if the client has setup a
+        // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
+        // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
+        val singleAppOptionDisabled =
+            !overrideDisableSingleAppOption &&
+                mediaProjectionConfig?.regionToCapture ==
+                    MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+        return if (singleAppOptionDisabled) {
+            context.getString(
+                R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
+                appName,
+            )
+        } else {
+            null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt
new file mode 100644
index 0000000..5a2d88c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package com.android.systemui.mediaprojection.permission
+
+import android.app.AlertDialog
+import android.content.Context
+import android.media.projection.MediaProjectionConfig
+import android.os.Bundle
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
+import com.android.systemui.res.R
+import java.util.function.Consumer
+
+/**
+ * Dialog to select screen recording options for sharing the screen to another app on the same
+ * device.
+ */
+class ShareToAppPermissionDialogDelegate(
+    context: Context,
+    mediaProjectionConfig: MediaProjectionConfig?,
+    private val onStartRecordingClicked:
+        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>>,
+    private val onCancelClicked: Runnable,
+    appName: String,
+    forceShowPartialScreenshare: Boolean,
+    hostUid: Int,
+    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
+) :
+    BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
+        createOptionList(
+            context,
+            appName,
+            mediaProjectionConfig,
+            overrideDisableSingleAppOption = forceShowPartialScreenshare,
+        ),
+        appName,
+        hostUid,
+        mediaProjectionMetricsLogger,
+    ) {
+    override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
+        super.onCreate(dialog, savedInstanceState)
+        // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
+        setDialogTitle(R.string.media_projection_entry_app_permission_dialog_title)
+        setStartButtonText(R.string.media_projection_entry_app_permission_dialog_continue)
+        setStartButtonOnClickListener {
+            // Note that it is important to run this callback before dismissing, so that the
+            // callback can disable the dialog exit animation if it wants to.
+            onStartRecordingClicked.accept(this)
+            dialog.dismiss()
+        }
+        setCancelButtonOnClickListener {
+            onCancelClicked.run()
+            dialog.dismiss()
+        }
+    }
+
+    companion object {
+        private fun createOptionList(
+            context: Context,
+            appName: String,
+            mediaProjectionConfig: MediaProjectionConfig?,
+            overrideDisableSingleAppOption: Boolean,
+        ): List<ScreenShareOption> {
+            val singleAppDisabledText =
+                MediaProjectionPermissionUtils.getSingleAppDisabledText(
+                    context,
+                    appName,
+                    mediaProjectionConfig,
+                    overrideDisableSingleAppOption,
+                )
+            val options =
+                listOf(
+                    ScreenShareOption(
+                        mode = SINGLE_APP,
+                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
+                        warningText =
+                            R.string
+                                .media_projection_entry_app_permission_dialog_warning_single_app,
+                        spinnerDisabledText = singleAppDisabledText,
+                    ),
+                    ScreenShareOption(
+                        mode = ENTIRE_SCREEN,
+                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
+                        warningText =
+                            R.string
+                                .media_projection_entry_app_permission_dialog_warning_entire_screen,
+                    )
+                )
+            return if (singleAppDisabledText != null) {
+                // Make sure "Entire screen" is the first option when "Single App" is disabled.
+                options.reversed()
+            } else {
+                options
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt
new file mode 100644
index 0000000..1ac3ccd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+package com.android.systemui.mediaprojection.permission
+
+import android.app.AlertDialog
+import android.content.Context
+import android.media.projection.MediaProjectionConfig
+import android.os.Bundle
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
+import com.android.systemui.mediaprojection.permission.MediaProjectionPermissionUtils.getSingleAppDisabledText
+import com.android.systemui.res.R
+import java.util.function.Consumer
+
+/** Dialog to select screen recording options for casting the screen to a different device. */
+class SystemCastPermissionDialogDelegate(
+    context: Context,
+    mediaProjectionConfig: MediaProjectionConfig?,
+    private val onStartRecordingClicked:
+        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>>,
+    private val onCancelClicked: Runnable,
+    appName: String,
+    forceShowPartialScreenshare: Boolean,
+    hostUid: Int,
+    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
+) :
+    BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
+        createOptionList(
+            context,
+            appName,
+            mediaProjectionConfig,
+            overrideDisableSingleAppOption = forceShowPartialScreenshare,
+        ),
+        appName,
+        hostUid,
+        mediaProjectionMetricsLogger,
+        dialogIconDrawable = R.drawable.ic_cast_connected,
+    ) {
+    override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
+        super.onCreate(dialog, savedInstanceState)
+        // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
+        setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
+        setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
+        setStartButtonOnClickListener {
+            // Note that it is important to run this callback before dismissing, so that the
+            // callback can disable the dialog exit animation if it wants to.
+            onStartRecordingClicked.accept(this)
+            dialog.dismiss()
+        }
+        setCancelButtonOnClickListener {
+            onCancelClicked.run()
+            dialog.dismiss()
+        }
+    }
+
+    companion object {
+        private fun createOptionList(
+            context: Context,
+            appName: String,
+            mediaProjectionConfig: MediaProjectionConfig?,
+            overrideDisableSingleAppOption: Boolean,
+        ): List<ScreenShareOption> {
+            val singleAppDisabledText =
+                getSingleAppDisabledText(
+                    context,
+                    appName,
+                    mediaProjectionConfig,
+                    overrideDisableSingleAppOption
+                )
+            val options =
+                listOf(
+                    ScreenShareOption(
+                        mode = SINGLE_APP,
+                        spinnerText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_option_text_single_app,
+                        warningText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_warning_single_app,
+                        spinnerDisabledText = singleAppDisabledText,
+                    ),
+                    ScreenShareOption(
+                        mode = ENTIRE_SCREEN,
+                        spinnerText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_option_text_entire_screen,
+                        warningText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_warning_entire_screen,
+                    )
+                )
+            return if (singleAppDisabledText != null) {
+                // Make sure "Entire screen" is the first option when "Single App" is disabled.
+                options.reversed()
+            } else {
+                options
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 9eca34f..0fe4d36 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -74,6 +74,7 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.contextualeducation.GestureType;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
@@ -1057,6 +1058,8 @@
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
                 mEdgeBackPlugin.onMotionEvent(ev);
                 dispatchToBackAnimation(ev);
+                mOverviewProxyService.updateContextualEduStats(mIsTrackpadThreeFingerSwipe,
+                        GestureType.BACK);
             }
             if (mLogGesture || mIsTrackpadThreeFingerSwipe) {
                 mDownPoint.set(ev.getX(), ev.getY());
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index c6f9ae8..045790c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -39,10 +39,10 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.collectIndexed
 import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.launch
 
 // TODO(b/http://b/299909989): Use QSTileViewModel directly after the rollout
@@ -87,8 +87,9 @@
                         }
                     }
                 }
-                // Warm up tile with some initial state
-                launch { qsTileViewModel.state.first() }
+                // Warm up tile with some initial state. Because `state` is a StateFlow with initial
+                // state `null`, we collect until it's not null.
+                launch { qsTileViewModel.state.takeWhile { it == null }.collect {} }
             }
 
         // QSTileHost doesn't call this when userId is initialized
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 371707d..15366d5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -89,6 +89,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.contextualeducation.GestureType;
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardWmStateRefactor;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -160,6 +162,8 @@
     private final NotificationShadeWindowController mStatusBarWinController;
     private final Provider<SceneInteractor> mSceneInteractor;
 
+    private final KeyboardTouchpadEduStatsInteractor mKeyboardTouchpadEduStatsInteractor;
+
     private final Runnable mConnectionRunnable = () ->
             internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
     private final ComponentName mRecentsComponentName;
@@ -661,7 +665,8 @@
             AssistUtils assistUtils,
             DumpManager dumpManager,
             Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder,
-            BroadcastDispatcher broadcastDispatcher
+            BroadcastDispatcher broadcastDispatcher,
+            KeyboardTouchpadEduStatsInteractor keyboardTouchpadEduStatsInteractor
     ) {
         // b/241601880: This component should only be running for primary users or
         // secondaryUsers when visibleBackgroundUsers are supported.
@@ -698,6 +703,7 @@
         mDisplayTracker = displayTracker;
         mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder;
         mBroadcastDispatcher = broadcastDispatcher;
+        mKeyboardTouchpadEduStatsInteractor = keyboardTouchpadEduStatsInteractor;
 
         if (!KeyguardWmStateRefactor.isEnabled()) {
             mSysuiUnlockAnimationController = sysuiUnlockAnimationController;
@@ -929,6 +935,19 @@
         return isEnabled() && !QuickStepContract.isLegacyMode(mNavBarMode);
     }
 
+    /**
+     * Updates contextual education stats when a gesture is triggered
+     * @param isTrackpadGesture indicates if the gesture is triggered by trackpad
+     * @param gestureType type of gesture triggered
+     */
+    public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) {
+        if (isTrackpadGesture) {
+            mKeyboardTouchpadEduStatsInteractor.updateShortcutTriggerTime(gestureType);
+        } else {
+            mKeyboardTouchpadEduStatsInteractor.incrementSignalCount(gestureType);
+        }
+    }
+
     public boolean isEnabled() {
         return mIsEnabled;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index bc8642c..a77375c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -19,7 +19,6 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
-import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
 import static com.android.systemui.Flags.screenshotSaveImageExporter;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
@@ -355,19 +354,9 @@
 
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
-            if (screenshotPrivateProfileAccessibilityAnnouncementFix()) {
-                mAnnouncementResolver.getScreenshotAnnouncement(
-                        screenshot.getUserHandle().getIdentifier(),
-                        mViewProxy::announceForAccessibility);
-            } else {
-                if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
-                    mViewProxy.announceForAccessibility(mContext.getResources().getString(
-                            R.string.screenshot_saving_work_profile_title));
-                } else {
-                    mViewProxy.announceForAccessibility(
-                            mContext.getResources().getString(R.string.screenshot_saving_title));
-                }
-            }
+            mAnnouncementResolver.getScreenshotAnnouncement(
+                    screenshot.getUserHandle().getIdentifier(),
+                    mViewProxy::announceForAccessibility);
         });
 
         mViewProxy.reset();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index ec529cd..540d4c4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -19,7 +19,6 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
-import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
 import static com.android.systemui.Flags.screenshotSaveImageExporter;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
@@ -355,19 +354,11 @@
 
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
-            if (screenshotPrivateProfileAccessibilityAnnouncementFix()) {
-                mAnnouncementResolver.getScreenshotAnnouncement(
-                        screenshot.getUserHandle().getIdentifier(),
-                        mViewProxy::announceForAccessibility);
-            } else {
-                if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
-                    mViewProxy.announceForAccessibility(mContext.getResources().getString(
-                            R.string.screenshot_saving_work_profile_title));
-                } else {
-                    mViewProxy.announceForAccessibility(
-                            mContext.getResources().getString(R.string.screenshot_saving_title));
-                }
-            }
+            mAnnouncementResolver.getScreenshotAnnouncement(
+                    screenshot.getUserHandle().getIdentifier(),
+                    announcement -> {
+                        mViewProxy.announceForAccessibility(announcement);
+                    });
         });
 
         mViewProxy.reset();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
index 29450a2..3fe3162 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult
 import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatched
 import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags
 import javax.inject.Inject
 import kotlinx.coroutines.flow.first
 
@@ -48,7 +48,7 @@
             return NotMatched(policy = NAME, reason = SHADE_EXPANDED)
         }
 
-        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+        if (DesktopModeFlags.DESKTOP_WINDOWING_MODE.isEnabled(context)) {
             content.rootTasks.firstOrNull()?.also {
                 if (it.windowingMode == WINDOWING_MODE_FREEFORM) {
                     return NotMatched(policy = NAME, reason = DESKTOP_MODE_ENABLED)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
index 134c983..d1a0a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
@@ -38,4 +39,7 @@
      * that is not considered "closing".
      */
     abstract val isAnyCloseAnimationRunning: StateFlow<Boolean>
+
+    /** Whether a short animation to expand or collapse is running after user input has ended. */
+    abstract val isAnyFlingAnimationRunning: Flow<Boolean>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
index f364d6d..dbc1b3b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 
 /** Implementation of ShadeAnimationInteractor for shadeless SysUI variants. */
 @SysUISingleton
@@ -29,4 +30,5 @@
     shadeAnimationRepository: ShadeAnimationRepository,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
     override val isAnyCloseAnimationRunning = MutableStateFlow(false)
+    override val isAnyFlingAnimationRunning = flowOf(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
index c4f4134..32d8659 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
 import com.android.systemui.shade.data.repository.ShadeRepository
 import javax.inject.Inject
+import kotlinx.coroutines.flow.map
 
 /** Implementation of ShadeAnimationInteractor compatible with NPVC. */
 @SysUISingleton
@@ -30,4 +31,5 @@
     shadeRepository: ShadeRepository,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
     override val isAnyCloseAnimationRunning = shadeRepository.legacyIsClosing
+    override val isAnyFlingAnimationRunning = shadeRepository.currentFling.map { it != null }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
index d9982e3..79a94a5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
@@ -36,12 +36,12 @@
 @SysUISingleton
 class ShadeAnimationInteractorSceneContainerImpl
 @Inject
+@OptIn(ExperimentalCoroutinesApi::class)
 constructor(
     @Background scope: CoroutineScope,
     shadeAnimationRepository: ShadeAnimationRepository,
     sceneInteractor: SceneInteractor,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
-    @OptIn(ExperimentalCoroutinesApi::class)
     override val isAnyCloseAnimationRunning =
         sceneInteractor.transitionState
             .flatMapLatest { state ->
@@ -62,4 +62,26 @@
             }
             .distinctUntilChanged()
             .stateIn(scope, SharingStarted.Eagerly, false)
+
+    override val isAnyFlingAnimationRunning =
+        sceneInteractor.transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle -> flowOf(false)
+                    is ObservableTransitionState.Transition ->
+                        if (
+                            state.isInitiatedByUserInput &&
+                                (state.fromScene == Scenes.Shade ||
+                                    state.toScene == Scenes.Shade ||
+                                    state.fromScene == Scenes.QuickSettings ||
+                                    state.toScene == Scenes.QuickSettings)
+                        ) {
+                            state.isUserInputOngoing.map { !it }
+                        } else {
+                            flowOf(false)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.Eagerly, false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
new file mode 100644
index 0000000..69ebb76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
@@ -0,0 +1,7 @@
+set noparent
+
+# Bug component: 78010
+
+caitlinshk@google.com
+evanlaird@google.com
+pixel@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index 59fd0ca..18ea0b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
@@ -59,12 +60,24 @@
                 when (state) {
                     is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden()
                     is OngoingCallModel.InCall -> {
+                        val icon =
+                            if (
+                                Flags.statusBarCallChipNotificationIcon() &&
+                                    state.notificationIconView != null
+                            ) {
+                                OngoingActivityChipModel.ChipIcon.StatusBarView(
+                                    state.notificationIconView
+                                )
+                            } else {
+                                OngoingActivityChipModel.ChipIcon.Basic(phoneIcon)
+                            }
+
                         // This block mimics OngoingCallController#updateChip.
                         if (state.startTimeMs <= 0L) {
                             // If the start time is invalid, don't show a timer and show just an
                             // icon. See b/192379214.
                             OngoingActivityChipModel.Shown.IconOnly(
-                                icon = phoneIcon,
+                                icon = icon,
                                 colors = ColorsModel.Themed,
                                 getOnClickListener(state),
                             )
@@ -73,7 +86,7 @@
                                 state.startTimeMs - systemClock.currentTimeMillis() +
                                     systemClock.elapsedRealtime()
                             OngoingActivityChipModel.Shown.Timer(
-                                icon = phoneIcon,
+                                icon = icon,
                                 colors = ColorsModel.Themed,
                                 startTimeMs = startTimeInElapsedRealtime,
                                 getOnClickListener(state),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index d9b0504..cf4e707 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -190,12 +190,14 @@
     ): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.Timer(
             icon =
-                Icon.Resource(
-                    CAST_TO_OTHER_DEVICE_ICON,
-                    // This string is "Casting screen"
-                    ContentDescription.Resource(
-                        R.string.cast_screen_to_other_device_chip_accessibility_label,
-                    ),
+                OngoingActivityChipModel.ChipIcon.Basic(
+                    Icon.Resource(
+                        CAST_TO_OTHER_DEVICE_ICON,
+                        // This string is "Casting screen"
+                        ContentDescription.Resource(
+                            R.string.cast_screen_to_other_device_chip_accessibility_label,
+                        ),
+                    )
                 ),
             colors = ColorsModel.Red,
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
@@ -213,10 +215,12 @@
     private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.IconOnly(
             icon =
-                Icon.Resource(
-                    CAST_TO_OTHER_DEVICE_ICON,
-                    // This string is just "Casting"
-                    ContentDescription.Resource(R.string.accessibility_casting),
+                OngoingActivityChipModel.ChipIcon.Basic(
+                    Icon.Resource(
+                        CAST_TO_OTHER_DEVICE_ICON,
+                        // This string is just "Casting"
+                        ContentDescription.Resource(R.string.accessibility_casting),
+                    )
                 ),
             colors = ColorsModel.Red,
             createDialogLaunchOnClickListener(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index 191c221..c5f78d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -21,11 +21,11 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.mediaprojection.MediaProjectionUtils.packageHasCastingCapabilities
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
 import com.android.systemui.statusbar.chips.StatusBarChipsLog
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
-import com.android.systemui.util.Utils
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -60,7 +60,7 @@
                     }
                     is MediaProjectionState.Projecting -> {
                         val type =
-                            if (isProjectionToOtherDevice(state.hostPackage)) {
+                            if (packageHasCastingCapabilities(packageManager, state.hostPackage)) {
                                 ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE
                             } else {
                                 ProjectionChipModel.Type.SHARE_TO_APP
@@ -86,19 +86,6 @@
         scope.launch { mediaProjectionRepository.stopProjecting() }
     }
 
-    /**
-     * Returns true iff projecting to the given [packageName] means that we're projecting to a
-     * *different* device (as opposed to projecting to some application on *this* device).
-     */
-    private fun isProjectionToOtherDevice(packageName: String?): Boolean {
-        // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
-        // different device or the same device, because headless remote display packages are the
-        // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
-        // because it means that any projection by those headless remote display packages will be
-        // marked as going to a different device, even if that isn't always true. See b/321078669.
-        return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
-    }
-
     companion object {
         private const val TAG = "MediaProjection"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index fcf3de4..6ba4fef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -78,11 +78,13 @@
                     is ScreenRecordChipModel.Recording -> {
                         OngoingActivityChipModel.Shown.Timer(
                             icon =
-                                Icon.Resource(
-                                    ICON,
-                                    ContentDescription.Resource(
-                                        R.string.screenrecord_ongoing_screen_only,
-                                    ),
+                                OngoingActivityChipModel.ChipIcon.Basic(
+                                    Icon.Resource(
+                                        ICON,
+                                        ContentDescription.Resource(
+                                            R.string.screenrecord_ongoing_screen_only,
+                                        ),
+                                    )
                                 ),
                             colors = ColorsModel.Red,
                             startTimeMs = systemClock.elapsedRealtime(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 85973fc..7897f93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -110,9 +110,11 @@
     ): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.Timer(
             icon =
-                Icon.Resource(
-                    SHARE_TO_APP_ICON,
-                    ContentDescription.Resource(R.string.share_to_app_chip_accessibility_label),
+                OngoingActivityChipModel.ChipIcon.Basic(
+                    Icon.Resource(
+                        SHARE_TO_APP_ICON,
+                        ContentDescription.Resource(R.string.share_to_app_chip_accessibility_label),
+                    )
                 ),
             colors = ColorsModel.Red,
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 17cf60b..26a2f91 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.chips.ui.model
 
 import android.view.View
+import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.statusbar.StatusBarIconView
 
 /** Model representing the display of an ongoing activity as a chip in the status bar. */
 sealed class OngoingActivityChipModel {
@@ -38,7 +40,7 @@
     /** This chip should be shown with the given information. */
     abstract class Shown(
         /** The icon to show on the chip. If null, no icon will be shown. */
-        open val icon: Icon?,
+        open val icon: ChipIcon?,
         /** What colors to use for the chip. */
         open val colors: ColorsModel,
         /**
@@ -50,7 +52,7 @@
 
         /** This chip shows only an icon and nothing else. */
         data class IconOnly(
-            override val icon: Icon,
+            override val icon: ChipIcon,
             override val colors: ColorsModel,
             override val onClickListener: View.OnClickListener?,
         ) : Shown(icon, colors, onClickListener) {
@@ -59,7 +61,7 @@
 
         /** The chip shows a timer, counting up from [startTimeMs]. */
         data class Timer(
-            override val icon: Icon,
+            override val icon: ChipIcon,
             override val colors: ColorsModel,
             /**
              * The time this event started, used to show the timer.
@@ -88,4 +90,23 @@
             override val logName = "Shown.Countdown"
         }
     }
+
+    /** Represents an icon to show on the chip. */
+    sealed interface ChipIcon {
+        /**
+         * The icon is a custom icon, which is set on [impl]. The icon was likely created by an
+         * external app.
+         */
+        data class StatusBarView(val impl: StatusBarIconView) : ChipIcon {
+            init {
+                check(Flags.statusBarCallChipNotificationIcon()) {
+                    "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " +
+                        "Flags.statusBarCallChipNotificationIcon is not enabled"
+                }
+            }
+        }
+
+        /** The icon is a basic resource or drawable icon that System UI created internally. */
+        data class Basic(val impl: Icon) : ChipIcon
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 5d2d56a..1008451 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -25,6 +25,8 @@
 import android.service.notification.StatusBarNotification
 import android.util.ArrayMap
 import com.android.app.tracing.traceSection
+import com.android.systemui.Flags
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -128,8 +130,14 @@
         return result
     }
 
-    private fun NotificationEntry.toModel(): ActiveNotificationModel =
-        existingModels.createOrReuse(
+    private fun NotificationEntry.toModel(): ActiveNotificationModel {
+        val statusBarChipIcon =
+            if (Flags.statusBarCallChipNotificationIcon()) {
+                icons.statusBarChipIcon
+            } else {
+                null
+            }
+        return existingModels.createOrReuse(
             key = key,
             groupKey = sbn.groupKey,
             whenTime = sbn.notification.`when`,
@@ -142,6 +150,7 @@
             aodIcon = icons.aodIcon?.sourceIcon,
             shelfIcon = icons.shelfIcon?.sourceIcon,
             statusBarIcon = icons.statusBarIcon?.sourceIcon,
+            statusBarChipIconView = statusBarChipIcon,
             uid = sbn.uid,
             packageName = sbn.packageName,
             contentIntent = sbn.notification.contentIntent,
@@ -150,6 +159,7 @@
             bucket = bucket,
             callType = sbn.toCallType(),
         )
+    }
 }
 
 private fun ActiveNotificationsStore.createOrReuse(
@@ -165,6 +175,7 @@
     aodIcon: Icon?,
     shelfIcon: Icon?,
     statusBarIcon: Icon?,
+    statusBarChipIconView: StatusBarIconView?,
     uid: Int,
     packageName: String,
     contentIntent: PendingIntent?,
@@ -187,6 +198,7 @@
             aodIcon = aodIcon,
             shelfIcon = shelfIcon,
             statusBarIcon = statusBarIcon,
+            statusBarChipIconView = statusBarChipIconView,
             uid = uid,
             instanceId = instanceId,
             isGroupSummary = isGroupSummary,
@@ -209,6 +221,7 @@
             aodIcon = aodIcon,
             shelfIcon = shelfIcon,
             statusBarIcon = statusBarIcon,
+            statusBarChipIconView = statusBarChipIconView,
             uid = uid,
             instanceId = instanceId,
             isGroupSummary = isGroupSummary,
@@ -232,6 +245,7 @@
     aodIcon: Icon?,
     shelfIcon: Icon?,
     statusBarIcon: Icon?,
+    statusBarChipIconView: StatusBarIconView?,
     uid: Int,
     packageName: String,
     contentIntent: PendingIntent?,
@@ -253,6 +267,7 @@
         aodIcon != this.aodIcon -> false
         shelfIcon != this.shelfIcon -> false
         statusBarIcon != this.statusBarIcon -> false
+        statusBarChipIconView != this.statusBarChipIconView -> false
         uid != this.uid -> false
         instanceId != this.instanceId -> false
         isGroupSummary != this.isGroupSummary -> false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 331d3cc..dc6ab41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -123,6 +123,13 @@
             // Construct the status bar icon view.
             val sbIcon = iconBuilder.createIconView(entry)
             sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
+            val sbChipIcon: StatusBarIconView?
+            if (Flags.statusBarCallChipNotificationIcon()) {
+                sbChipIcon = iconBuilder.createIconView(entry)
+                sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
+            } else {
+                sbChipIcon = null
+            }
 
             // Construct the shelf icon view.
             val shelfIcon = iconBuilder.createIconView(entry)
@@ -139,9 +146,19 @@
 
             try {
                 setIcon(entry, normalIconDescriptor, sbIcon)
+                if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) {
+                    setIcon(entry, normalIconDescriptor, sbChipIcon)
+                }
                 setIcon(entry, sensitiveIconDescriptor, shelfIcon)
                 setIcon(entry, sensitiveIconDescriptor, aodIcon)
-                entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons)
+                entry.icons =
+                    IconPack.buildPack(
+                        sbIcon,
+                        sbChipIcon,
+                        shelfIcon,
+                        aodIcon,
+                        entry.icons,
+                    )
             } catch (e: InflationException) {
                 entry.icons = IconPack.buildEmptyPack(entry.icons)
                 throw e
@@ -182,6 +199,11 @@
                 setIcon(entry, normalIconDescriptor, it)
             }
 
+            entry.icons.statusBarChipIcon?.let {
+                it.setNotification(entry.sbn, notificationContentDescription)
+                setIcon(entry, normalIconDescriptor, it)
+            }
+
             entry.icons.shelfIcon?.let {
                 it.setNotification(entry.sbn, notificationContentDescription)
                 setIcon(entry, sensitiveIconDescriptor, it)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
index d029ce7..611cebc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
@@ -29,6 +29,7 @@
 
     private final boolean mAreIconsAvailable;
     @Nullable private final StatusBarIconView mStatusBarIcon;
+    @Nullable private final StatusBarIconView mStatusBarChipIcon;
     @Nullable private final StatusBarIconView mShelfIcon;
     @Nullable private final StatusBarIconView mAodIcon;
 
@@ -43,7 +44,7 @@
      * haven't been inflated yet or there was an error while inflating them).
      */
     public static IconPack buildEmptyPack(@Nullable IconPack fromSource) {
-        return new IconPack(false, null, null, null, fromSource);
+        return new IconPack(false, null, null, null, null, fromSource);
     }
 
     /**
@@ -51,20 +52,23 @@
      */
     public static IconPack buildPack(
             @NonNull StatusBarIconView statusBarIcon,
+            @Nullable StatusBarIconView statusBarChipIcon,
             @NonNull StatusBarIconView shelfIcon,
             @NonNull StatusBarIconView aodIcon,
             @Nullable IconPack source) {
-        return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, source);
+        return new IconPack(true, statusBarIcon, statusBarChipIcon, shelfIcon, aodIcon, source);
     }
 
     private IconPack(
             boolean areIconsAvailable,
             @Nullable StatusBarIconView statusBarIcon,
+            @Nullable StatusBarIconView statusBarChipIcon,
             @Nullable StatusBarIconView shelfIcon,
             @Nullable StatusBarIconView aodIcon,
             @Nullable IconPack source) {
         mAreIconsAvailable = areIconsAvailable;
         mStatusBarIcon = statusBarIcon;
+        mStatusBarChipIcon = statusBarChipIcon;
         mShelfIcon = shelfIcon;
         mAodIcon = aodIcon;
         if (source != null) {
@@ -79,6 +83,17 @@
     }
 
     /**
+     * The version of the notification icon that appears inside a chip within the status bar.
+     *
+     * Separate from {@link #getStatusBarIcon()} so that we don't have to worry about detaching and
+     * re-attaching the same view when the chip appears and hides.
+     */
+    @Nullable
+    public StatusBarIconView getStatusBarChipIcon() {
+        return mStatusBarChipIcon;
+    }
+
+    /**
      * The version of the icon that appears in the "shelf" at the bottom of the notification shade.
      * In general, this icon also appears somewhere on the notification and is "sucked" into the
      * shelf as the scrolls beyond it.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index a6ca3ab..17f401a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -19,6 +19,9 @@
 import android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST
 import android.app.Notification
 import android.app.Notification.BubbleMetadata
+import android.app.Notification.CATEGORY_ALARM
+import android.app.Notification.CATEGORY_CAR_EMERGENCY
+import android.app.Notification.CATEGORY_CAR_WARNING
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.Notification.VISIBILITY_PRIVATE
@@ -42,6 +45,7 @@
 import android.service.notification.Flags
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -307,6 +311,9 @@
         ALLOW_CALLSTYLE,
         ALLOW_CATEGORY_REMINDER,
         ALLOW_CATEGORY_EVENT,
+        ALLOW_CATEGORY_ALARM,
+        ALLOW_CATEGORY_CAR_EMERGENCY,
+        ALLOW_CATEGORY_CAR_WARNING,
         ALLOW_FSI_WITH_PERMISSION_ON,
         ALLOW_COLORIZED,
         ALLOW_EMERGENCY,
@@ -333,8 +340,13 @@
         @UiEvent(doc = "HUN allowed during avalanche because it is colorized.")
         AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED(1832),
         @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.")
-        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833);
-
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833),
+        @UiEvent(doc = "HUN allowed during avalanche because it is an alarm.")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM(1867),
+        @UiEvent(doc = "HUN allowed during avalanche because it is a car emergency.")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY(1868),
+        @UiEvent(doc = "HUN allowed during avalanche because it is a car warning")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING(1869);
         override fun getId(): Int {
             return id
         }
@@ -423,6 +435,22 @@
             return State.ALLOW_CATEGORY_REMINDER
         }
 
+        if (entry.sbn.notification.category == CATEGORY_ALARM) {
+            uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM)
+            return State.ALLOW_CATEGORY_ALARM
+        }
+
+        if (entry.sbn.notification.category == CATEGORY_CAR_EMERGENCY) {
+            uiEventLogger.log(
+                    AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY)
+            return State.ALLOW_CATEGORY_CAR_EMERGENCY
+        }
+
+        if (entry.sbn.notification.category == CATEGORY_CAR_WARNING) {
+            uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING)
+            return State.ALLOW_CATEGORY_CAR_WARNING
+        }
+
         if (entry.sbn.notification.category == CATEGORY_EVENT) {
             uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT)
             return State.ALLOW_CATEGORY_EVENT
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index 6960791..cf19938 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -17,6 +17,7 @@
 
 import android.app.PendingIntent
 import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.stack.PriorityBucket
 
 /**
@@ -59,6 +60,8 @@
     val shelfIcon: Icon?,
     /** Icon to display in the status bar. */
     val statusBarIcon: Icon?,
+    /** Icon to display in the status bar chip. */
+    val statusBarChipIconView: StatusBarIconView?,
     /** The notifying app's [packageName]'s uid. */
     val uid: Int,
     /** The notifying app's packageName. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 37fdaeb..6d3cad5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2995,7 +2995,7 @@
                 @Override
                 public void onFalse() {
                     // Hides quick settings, bouncer, and quick-quick settings.
-                    mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
+                    mStatusBarKeyguardViewManager.reset(true);
                 }
             };
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
index 4657e9b..92a333e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
@@ -1,6 +1,6 @@
 per-file *Notification* = set noparent
 per-file *Notification* = file:../notification/OWNERS
 
-per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com
+per-file NotificationIcon* = file:../OWNERS
 
 per-file NotificationShadeWindowControllerImpl.java = dupin@google.com, cinek@google.com, beverlyt@google.com, pixel@google.com, juliacr@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 0b8f18e..2d775b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
      * Shows the notification keyguard or the bouncer depending on
      * {@link #needsFullscreenBouncer()}.
      */
-    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
         boolean isDozing = mDozing;
         if (Flags.simPinRaceConditionOnRestart()) {
             KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,12 +734,8 @@
                         mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
                     }
                 }
-            } else if (!isFalsingReset) {
-                // Falsing resets can cause this to flicker, so don't reset in this case
-                Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
-                mPrimaryBouncerInteractor.hide();
-                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
-
+            } else {
+                Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
             }
         } else {
             mCentralSurfaces.showKeyguard();
@@ -961,10 +957,6 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
-        reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
-    }
-
-    public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
             final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
@@ -976,7 +968,7 @@
                     hideBouncer(false /* destroyView */);
                 }
             } else {
-                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
+                showBouncerOrKeyguard(hideBouncerWhenShowing);
             }
             if (hideBouncerWhenShowing) {
                 hideAlternateBouncer(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 7af0666..4368239 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
@@ -114,6 +115,9 @@
                         CallNotificationInfo(
                             entry.sbn.key,
                             entry.sbn.notification.getWhen(),
+                            // In this old listener pattern, we don't have access to the
+                            // notification icon.
+                            notificationIconView = null,
                             entry.sbn.notification.contentIntent,
                             entry.sbn.uid,
                             entry.sbn.notification.extras.getInt(
@@ -223,8 +227,15 @@
                 callNotificationInfo
                     // This shouldn't happen, but protect against it in case
                     ?: return OngoingCallModel.NoCall
+            val icon =
+                if (Flags.statusBarCallChipNotificationIcon()) {
+                    currentInfo.notificationIconView
+                } else {
+                    null
+                }
             return OngoingCallModel.InCall(
                 startTimeMs = currentInfo.callStartTime,
+                notificationIconView = icon,
                 intent = currentInfo.intent,
             )
         } else {
@@ -260,6 +271,7 @@
                 CallNotificationInfo(
                     notifModel.key,
                     notifModel.whenTime,
+                    notifModel.statusBarChipIconView,
                     notifModel.contentIntent,
                     notifModel.uid,
                     isOngoing = true,
@@ -407,6 +419,8 @@
     private data class CallNotificationInfo(
         val key: String,
         val callStartTime: Long,
+        /** The icon set as the [android.app.Notification.getSmallIcon] field. */
+        val notificationIconView: StatusBarIconView?,
         val intent: PendingIntent?,
         val uid: Int,
         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
index 2c48487..34bff80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone.ongoingcall.shared.model
 
 import android.app.PendingIntent
+import com.android.systemui.statusbar.StatusBarIconView
 
 /** Represents the state of any ongoing calls. */
 sealed interface OngoingCallModel {
@@ -31,7 +32,13 @@
      *   [com.android.systemui.util.time.SystemClock.currentTimeMillis], **not**
      *   [com.android.systemui.util.time.SystemClock.elapsedRealtime]. This value can be 0 if the
      *   user has started an outgoing call that hasn't been answered yet - see b/192379214.
+     * @property notificationIconView the [android.app.Notification.getSmallIcon] that's set on the
+     *   call notification. We may use this icon in the chip instead of the default phone icon.
      * @property intent the intent associated with the call notification.
      */
-    data class InCall(val startTimeMs: Long, val intent: PendingIntent?) : OngoingCallModel
+    data class InCall(
+        val startTimeMs: Long,
+        val notificationIconView: StatusBarIconView?,
+        val intent: PendingIntent?,
+    ) : OngoingCallModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index 16bd7f8..d46aaf4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -18,9 +18,12 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
+import android.annotation.IdRes
 import android.content.res.ColorStateList
 import android.graphics.drawable.GradientDrawable
 import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.lifecycle.Lifecycle
@@ -31,6 +34,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.ui.binder.ChipChronometerBinder
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -90,7 +94,7 @@
                 if (Flags.statusBarScreenSharingChips()) {
                     val chipView: View = view.requireViewById(R.id.ongoing_activity_chip)
                     val chipContext = chipView.context
-                    val chipIconView: ImageView =
+                    val chipDefaultIconView: ImageView =
                         chipView.requireViewById(R.id.ongoing_activity_chip_icon)
                     val chipTimeView: ChipChronometer =
                         chipView.requireViewById(R.id.ongoing_activity_chip_time)
@@ -105,16 +109,25 @@
                             when (chipModel) {
                                 is OngoingActivityChipModel.Shown -> {
                                     // Data
-                                    IconViewBinder.bindNullable(chipModel.icon, chipIconView)
+                                    setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView)
                                     setChipMainContent(chipModel, chipTextView, chipTimeView)
                                     chipView.setOnClickListener(chipModel.onClickListener)
+                                    updateChipPadding(
+                                        chipModel,
+                                        chipBackgroundView,
+                                        chipTextView,
+                                        chipTimeView,
+                                    )
 
                                     // Accessibility
                                     setChipAccessibility(chipModel, chipView, chipBackgroundView)
 
                                     // Colors
                                     val textColor = chipModel.colors.text(chipContext)
-                                    chipIconView.imageTintList = ColorStateList.valueOf(textColor)
+                                    chipDefaultIconView.imageTintList =
+                                        ColorStateList.valueOf(textColor)
+                                    chipBackgroundView.getCustomIconView()?.imageTintList =
+                                        ColorStateList.valueOf(textColor)
                                     chipTimeView.setTextColor(textColor)
                                     chipTextView.setTextColor(textColor)
                                     (chipBackgroundView.background as GradientDrawable).color =
@@ -151,6 +164,69 @@
         }
     }
 
+    private fun setChipIcon(
+        chipModel: OngoingActivityChipModel.Shown,
+        backgroundView: ChipBackgroundContainer,
+        defaultIconView: ImageView,
+    ) {
+        // Always remove any previously set custom icon. If we have a new custom icon, we'll re-add
+        // it.
+        backgroundView.removeView(backgroundView.getCustomIconView())
+
+        when (val icon = chipModel.icon) {
+            null -> {
+                defaultIconView.visibility = View.GONE
+            }
+            is OngoingActivityChipModel.ChipIcon.Basic -> {
+                IconViewBinder.bind(icon.impl, defaultIconView)
+                defaultIconView.visibility = View.VISIBLE
+            }
+            is OngoingActivityChipModel.ChipIcon.StatusBarView -> {
+                // Hide the default icon since we'll show this custom icon instead.
+                defaultIconView.visibility = View.GONE
+
+                // Add the new custom icon:
+                // 1. Set up the right visual params.
+                val iconView = icon.impl
+                with(iconView) {
+                    id = CUSTOM_ICON_VIEW_ID
+                    // TODO(b/354930838): Update the content description to not include "phone" and
+                    // maybe include the app name.
+                    contentDescription =
+                        context.resources.getString(R.string.ongoing_phone_call_content_description)
+                }
+
+                // 2. If we just reinflated the view, we may need to detach the icon view from the
+                // old chip before we reattach it to the new one.
+                // See also: NotificationIconContainerViewBinder#bindIcons.
+                val currentParent = iconView.parent as? ViewGroup
+                if (currentParent != null && currentParent != backgroundView) {
+                    currentParent.removeView(iconView)
+                    currentParent.removeTransientView(iconView)
+                }
+
+                // 3: Add the icon as the starting view.
+                backgroundView.addView(
+                    iconView,
+                    /* index= */ 0,
+                    generateCustomIconLayoutParams(iconView),
+                )
+            }
+        }
+    }
+
+    private fun View.getCustomIconView(): StatusBarIconView? {
+        return this.findViewById(CUSTOM_ICON_VIEW_ID)
+    }
+
+    private fun generateCustomIconLayoutParams(iconView: ImageView): FrameLayout.LayoutParams {
+        val customIconSize =
+            iconView.context.resources.getDimensionPixelSize(
+                R.dimen.ongoing_activity_chip_embedded_padding_icon_size
+            )
+        return FrameLayout.LayoutParams(customIconSize, customIconSize)
+    }
+
     private fun setChipMainContent(
         chipModel: OngoingActivityChipModel.Shown,
         chipTextView: TextView,
@@ -180,37 +256,93 @@
                 chipTimeView.visibility = View.GONE
             }
         }
-        updateChipTextPadding(chipModel, chipTextView, chipTimeView)
     }
 
-    private fun updateChipTextPadding(
+    private fun updateChipPadding(
         chipModel: OngoingActivityChipModel.Shown,
+        backgroundView: View,
         chipTextView: TextView,
         chipTimeView: ChipChronometer,
     ) {
-        val requiresPadding = chipModel.icon != null
-        if (requiresPadding) {
-            chipTextView.addChipTextPaddingStart()
-            chipTimeView.addChipTextPaddingStart()
+        if (chipModel.icon != null) {
+            if (chipModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView) {
+                // If the icon is a custom [StatusBarIconView], then it should've come from
+                // `Notification.smallIcon`, which is required to embed its own paddings. We need to
+                // adjust the other paddings to make everything look good :)
+                backgroundView.setBackgroundPaddingForEmbeddedPaddingIcon()
+                chipTextView.setTextPaddingForEmbeddedPaddingIcon()
+                chipTimeView.setTextPaddingForEmbeddedPaddingIcon()
+            } else {
+                backgroundView.setBackgroundPaddingForNormalIcon()
+                chipTextView.setTextPaddingForNormalIcon()
+                chipTimeView.setTextPaddingForNormalIcon()
+            }
         } else {
-            chipTextView.removeChipTextPaddingStart()
-            chipTimeView.removeChipTextPaddingStart()
+            backgroundView.setBackgroundPaddingForNoIcon()
+            chipTextView.setTextPaddingForNoIcon()
+            chipTimeView.setTextPaddingForNoIcon()
         }
     }
 
-    private fun View.addChipTextPaddingStart() {
+    private fun View.setTextPaddingForEmbeddedPaddingIcon() {
+        val newPaddingEnd =
+            context.resources.getDimensionPixelSize(
+                R.dimen.ongoing_activity_chip_text_end_padding_for_embedded_padding_icon
+            )
+        setPaddingRelative(
+            // The icon should embed enough padding between the icon and time view.
+            /* start= */ 0,
+            this.paddingTop,
+            newPaddingEnd,
+            this.paddingBottom,
+        )
+    }
+
+    private fun View.setTextPaddingForNormalIcon() {
         this.setPaddingRelative(
             this.context.resources.getDimensionPixelSize(
                 R.dimen.ongoing_activity_chip_icon_text_padding
             ),
             paddingTop,
-            paddingEnd,
+            // The background view will contain the right end padding.
+            /* end= */ 0,
             paddingBottom,
         )
     }
 
-    private fun View.removeChipTextPaddingStart() {
-        this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
+    private fun View.setTextPaddingForNoIcon() {
+        // The background view will have even start & end paddings, so we don't want the text view
+        // to add any additional padding.
+        this.setPaddingRelative(/* start= */ 0, paddingTop, /* end= */ 0, paddingBottom)
+    }
+
+    private fun View.setBackgroundPaddingForEmbeddedPaddingIcon() {
+        val sidePadding =
+            context.resources.getDimensionPixelSize(
+                R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
+            )
+        setPaddingRelative(
+            sidePadding,
+            paddingTop,
+            sidePadding,
+            paddingBottom,
+        )
+    }
+
+    private fun View.setBackgroundPaddingForNormalIcon() {
+        val sidePadding =
+            context.resources.getDimensionPixelSize(R.dimen.ongoing_activity_chip_side_padding)
+        setPaddingRelative(
+            sidePadding,
+            paddingTop,
+            sidePadding,
+            paddingBottom,
+        )
+    }
+
+    private fun View.setBackgroundPaddingForNoIcon() {
+        // The padding for the normal icon is also appropriate for no icon.
+        setBackgroundPaddingForNormalIcon()
     }
 
     private fun setChipAccessibility(
@@ -269,6 +401,10 @@
             )
             .start()
     }
+
+    companion object {
+        @IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon
+    }
 }
 
 /** Listener for various events that may affect the status bar's visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index ed3355e..e3666ce 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -16,38 +16,26 @@
 
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
-import android.view.MotionEvent
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import kotlin.math.abs
 
+/** Monitors for touchpad back gesture, that is three fingers swiping left or right */
 class BackGestureMonitor(
     override val gestureDistanceThresholdPx: Int,
     override val gestureStateChangedCallback: (GestureState) -> Unit
-) : TouchpadGestureMonitor {
-
-    private var xStart = 0f
-
-    override fun processTouchpadEvent(event: MotionEvent) {
-        val action = event.actionMasked
-        when (action) {
-            MotionEvent.ACTION_DOWN -> {
-                if (isThreeFingerTouchpadSwipe(event)) {
-                    xStart = event.x
-                    gestureStateChangedCallback(IN_PROGRESS)
+) :
+    TouchpadGestureMonitor by ThreeFingerGestureMonitor(
+        gestureDistanceThresholdPx = gestureDistanceThresholdPx,
+        gestureStateChangedCallback = gestureStateChangedCallback,
+        donePredicate =
+            object : GestureDonePredicate {
+                override fun wasGestureDone(
+                    startX: Float,
+                    startY: Float,
+                    endX: Float,
+                    endY: Float
+                ): Boolean {
+                    val distance = abs(endX - startX)
+                    return distance >= gestureDistanceThresholdPx
                 }
             }
-            MotionEvent.ACTION_UP -> {
-                if (isThreeFingerTouchpadSwipe(event)) {
-                    val distance = abs(event.x - xStart)
-                    if (distance >= gestureDistanceThresholdPx) {
-                        gestureStateChangedCallback(FINISHED)
-                    } else {
-                        gestureStateChangedCallback(NOT_STARTED)
-                    }
-                }
-            }
-        }
-    }
-}
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
new file mode 100644
index 0000000..a410f99
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.touchpad.tutorial.ui.gesture
+
+/** Monitors for touchpad home gesture, that is three fingers swiping up */
+class HomeGestureMonitor(
+    override val gestureDistanceThresholdPx: Int,
+    override val gestureStateChangedCallback: (GestureState) -> Unit
+) :
+    TouchpadGestureMonitor by ThreeFingerGestureMonitor(
+        gestureDistanceThresholdPx = gestureDistanceThresholdPx,
+        gestureStateChangedCallback = gestureStateChangedCallback,
+        donePredicate =
+            object : GestureDonePredicate {
+                override fun wasGestureDone(
+                    startX: Float,
+                    startY: Float,
+                    endX: Float,
+                    endY: Float
+                ): Boolean {
+                    val distance = startY - endY
+                    return distance >= gestureDistanceThresholdPx
+                }
+            }
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureMonitor.kt
new file mode 100644
index 0000000..377977c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureMonitor.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+
+interface GestureDonePredicate {
+    /**
+     * Should return if gesture was finished. The only events this predicate receives are ACTION_UP.
+     */
+    fun wasGestureDone(startX: Float, startY: Float, endX: Float, endY: Float): Boolean
+}
+
+/** Common implementation for all three-finger gesture monitors */
+class ThreeFingerGestureMonitor(
+    override val gestureDistanceThresholdPx: Int,
+    override val gestureStateChangedCallback: (GestureState) -> Unit,
+    private val donePredicate: GestureDonePredicate
+) : TouchpadGestureMonitor {
+
+    private var xStart = 0f
+    private var yStart = 0f
+
+    override fun processTouchpadEvent(event: MotionEvent) {
+        val action = event.actionMasked
+        when (action) {
+            MotionEvent.ACTION_DOWN -> {
+                if (isThreeFingerTouchpadSwipe(event)) {
+                    xStart = event.x
+                    yStart = event.y
+                    gestureStateChangedCallback(GestureState.IN_PROGRESS)
+                }
+            }
+            MotionEvent.ACTION_UP -> {
+                if (isThreeFingerTouchpadSwipe(event)) {
+                    if (donePredicate.wasGestureDone(xStart, yStart, event.x, event.y)) {
+                        gestureStateChangedCallback(GestureState.FINISHED)
+                    } else {
+                        gestureStateChangedCallback(GestureState.NOT_STARTED)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 1e3ee28..dc69cda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -35,7 +35,6 @@
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.ViewUtils
-import android.view.KeyEvent
 import android.view.View
 import android.view.WindowInsets
 import android.view.WindowManager
@@ -202,8 +201,7 @@
         val root = container.rootView
 
         // Simulate back invocation
-        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
-        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
+        container.onBackInvoked()
         waitForIdleSync()
 
         assertThat(container.parent).isNull()
@@ -217,8 +215,7 @@
         val root = container.rootView
 
         // Simulate back invocation
-        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
-        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
+        container.onBackInvoked()
         waitForIdleSync()
 
         assertThat(container.parent).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index e603db4..6ba9b32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -44,7 +44,6 @@
 import android.view.Surface
 import androidx.test.filters.SmallTest
 import com.android.app.activityTaskManager
-import com.android.systemui.Flags.FLAG_BP_TALKBACK
 import com.android.systemui.Flags.FLAG_CONSTRAINT_BP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
@@ -100,9 +99,10 @@
 private const val REQUEST_ID = 4L
 private const val CHALLENGE = 2L
 private const val DELAY = 1000L
-private const val OP_PACKAGE_NAME = "biometric.testapp"
+private const val OP_PACKAGE_NAME_WITH_APP_LOGO = "biometric.testapp"
 private const val OP_PACKAGE_NAME_NO_ICON = "biometric.testapp.noicon"
 private const val OP_PACKAGE_NAME_CAN_NOT_BE_FOUND = "can.not.be.found"
+private const val OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO = "should.use.activiy.logo"
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -112,19 +112,19 @@
     @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
 
     @Mock private lateinit var authController: AuthController
-    @Mock private lateinit var applicationInfoWithIcon: ApplicationInfo
-    @Mock private lateinit var applicationInfoNoIcon: ApplicationInfo
+    @Mock private lateinit var applicationInfoWithIconAndDescription: ApplicationInfo
+    @Mock private lateinit var applicationInfoNoIconOrDescription: ApplicationInfo
     @Mock private lateinit var activityInfo: ActivityInfo
     @Mock private lateinit var runningTaskInfo: RunningTaskInfo
 
-    private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android)
-    private val defaultLogoIconWithOverrides = context.getDrawable(R.drawable.ic_add)
+    private val defaultLogoIconFromAppInfo = context.getDrawable(R.drawable.ic_android)
+    private val defaultLogoIconFromActivityInfo = context.getDrawable(R.drawable.ic_add)
     private val logoResFromApp = R.drawable.ic_cake
     private val logoDrawableFromAppRes = context.getDrawable(logoResFromApp)
     private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565)
-    private val defaultLogoDescription = "Test Android App"
+    private val defaultLogoDescriptionFromAppInfo = "Test Android App"
+    private val defaultLogoDescriptionFromActivityInfo = "Test Coke App"
     private val logoDescriptionFromApp = "Test Cake App"
-    private val packageNameForLogoWithOverrides = "should.use.overridden.logo"
     /** Prompt panel size padding */
     private val smallHorizontalGuidelinePadding =
         context.resources.getDimensionPixelSize(
@@ -172,16 +172,21 @@
 
         // Set up default logo info and app customized info
         whenever(kosmos.packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_ICON), anyInt()))
-            .thenReturn(applicationInfoNoIcon)
-        whenever(kosmos.packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME), anyInt()))
-            .thenReturn(applicationInfoWithIcon)
+            .thenReturn(applicationInfoNoIconOrDescription)
         whenever(
                 kosmos.packageManager.getApplicationInfo(
-                    eq(packageNameForLogoWithOverrides),
+                    eq(OP_PACKAGE_NAME_WITH_APP_LOGO),
                     anyInt()
                 )
             )
-            .thenReturn(applicationInfoWithIcon)
+            .thenReturn(applicationInfoWithIconAndDescription)
+        whenever(
+                kosmos.packageManager.getApplicationInfo(
+                    eq(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
+                    anyInt()
+                )
+            )
+            .thenReturn(applicationInfoWithIconAndDescription)
         whenever(
                 kosmos.packageManager.getApplicationInfo(
                     eq(OP_PACKAGE_NAME_CAN_NOT_BE_FOUND),
@@ -191,19 +196,27 @@
             .thenThrow(NameNotFoundException())
 
         whenever(kosmos.packageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo)
-        whenever(kosmos.iconProvider.getIcon(activityInfo)).thenReturn(defaultLogoIconWithOverrides)
-        whenever(kosmos.packageManager.getApplicationIcon(applicationInfoWithIcon))
-            .thenReturn(defaultLogoIcon)
-        whenever(kosmos.packageManager.getApplicationLabel(applicationInfoWithIcon))
-            .thenReturn(defaultLogoDescription)
+        whenever(kosmos.iconProvider.getIcon(activityInfo))
+            .thenReturn(defaultLogoIconFromActivityInfo)
+        whenever(activityInfo.loadLabel(kosmos.packageManager))
+            .thenReturn(defaultLogoDescriptionFromActivityInfo)
+
+        whenever(kosmos.packageManager.getApplicationIcon(applicationInfoWithIconAndDescription))
+            .thenReturn(defaultLogoIconFromAppInfo)
+        whenever(kosmos.packageManager.getApplicationLabel(applicationInfoWithIconAndDescription))
+            .thenReturn(defaultLogoDescriptionFromAppInfo)
+        whenever(kosmos.packageManager.getApplicationIcon(applicationInfoNoIconOrDescription))
+            .thenReturn(null)
+        whenever(kosmos.packageManager.getApplicationLabel(applicationInfoNoIconOrDescription))
+            .thenReturn("")
         whenever(kosmos.packageManager.getUserBadgedIcon(any(), any())).then { it.getArgument(0) }
         whenever(kosmos.packageManager.getUserBadgedLabel(any(), any())).then { it.getArgument(0) }
 
         context.setMockPackageManager(kosmos.packageManager)
         overrideResource(logoResFromApp, logoDrawableFromAppRes)
         overrideResource(
-            R.array.biometric_dialog_package_names_for_logo_with_overrides,
-            arrayOf(packageNameForLogoWithOverrides)
+            R.array.config_useActivityLogoForBiometricPrompt,
+            arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO)
         )
 
         overrideResource(R.dimen.biometric_dialog_fingerprint_icon_width, mockFingerprintIconWidth)
@@ -1356,7 +1369,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_BP_TALKBACK)
     fun hint_for_talkback_guidance() = runGenericTest {
         val hint by collectLastValue(kosmos.promptViewModel.accessibilityHint)
 
@@ -1379,7 +1391,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_BP_TALKBACK)
     fun no_hint_for_talkback_guidance_after_auth() = runGenericTest {
         val hint by collectLastValue(kosmos.promptViewModel.accessibilityHint)
 
@@ -1440,36 +1451,41 @@
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logo_nullIfPkgNameNotFound() =
         runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
-            val logo by collectLastValue(kosmos.promptViewModel.logo)
-            assertThat(logo).isNull()
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo).isNotNull()
+            assertThat(logoInfo!!.first).isNull()
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
-    fun logo_defaultWithOverrides() =
-        runGenericTest(packageName = packageNameForLogoWithOverrides) {
-            val logo by collectLastValue(kosmos.promptViewModel.logo)
+    fun logo_defaultFromActivityInfo() =
+        runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
 
-            // 1. PM.getApplicationInfo(packageNameForLogoWithOverrides) is set to return
-            // applicationInfoWithIcon with defaultLogoIcon,
-            // 2. iconProvider.getIcon() is set to return defaultLogoIconForGMSCore
-            // For the apps with packageNameForLogoWithOverrides, 2 should be called instead of 1
-            assertThat(logo).isEqualTo(defaultLogoIconWithOverrides)
+            // 1. PM.getApplicationInfo(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) is set to return
+            // applicationInfoWithIconAndDescription with "defaultLogoIconFromAppInfo",
+            // 2. iconProvider.getIcon(activityInfo) is set to return
+            // "defaultLogoIconFromActivityInfo"
+            // For the apps with OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO, 2 should be called instead of 1
+            assertThat(logoInfo).isNotNull()
+            assertThat(logoInfo!!.first).isEqualTo(defaultLogoIconFromActivityInfo)
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logo_defaultIsNull() =
         runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
-            val logo by collectLastValue(kosmos.promptViewModel.logo)
-            assertThat(logo).isNull()
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo).isNotNull()
+            assertThat(logoInfo!!.first).isNull()
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logo_default() = runGenericTest {
-        val logo by collectLastValue(kosmos.promptViewModel.logo)
-        assertThat(logo).isEqualTo(defaultLogoIcon)
+        val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+        assertThat(logoInfo).isNotNull()
+        assertThat(logoInfo!!.first).isEqualTo(defaultLogoIconFromAppInfo)
     }
 
     @Test
@@ -1477,47 +1493,60 @@
     fun logo_resSetByApp() =
         runGenericTest(logoRes = logoResFromApp) {
             val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap()
-            val logo by collectLastValue(kosmos.promptViewModel.logo)
-            assertThat((logo as BitmapDrawable).bitmap.sameAs(expectedBitmap)).isTrue()
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo).isNotNull()
+            assertThat((logoInfo!!.first as BitmapDrawable).bitmap.sameAs(expectedBitmap)).isTrue()
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logo_bitmapSetByApp() =
         runGenericTest(logoBitmap = logoBitmapFromApp) {
-            val logo by collectLastValue(kosmos.promptViewModel.logo)
-            assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp)
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat((logoInfo!!.first as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp)
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logoDescription_emptyIfPkgNameNotFound() =
         runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
-            val logoDescription by collectLastValue(kosmos.promptViewModel.logoDescription)
-            assertThat(logoDescription).isEqualTo("")
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo!!.second).isEqualTo("")
+        }
+
+    @Test
+    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+    fun logoDescription_defaultFromActivityInfo() =
+        runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            // 1. PM.getApplicationInfo(packageNameForLogoWithOverrides) is set to return
+            // applicationInfoWithIconAndDescription with defaultLogoDescription,
+            // 2. activityInfo.loadLabel() is set to return defaultLogoDescriptionWithOverrides
+            // For the apps with packageNameForLogoWithOverrides, 2 should be called instead of 1
+            assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromActivityInfo)
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logoDescription_defaultIsEmpty() =
         runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
-            val logoDescription by collectLastValue(kosmos.promptViewModel.logoDescription)
-            assertThat(logoDescription).isEqualTo("")
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo!!.second).isEqualTo("")
         }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logoDescription_default() = runGenericTest {
-        val logoDescription by collectLastValue(kosmos.promptViewModel.logoDescription)
-        assertThat(logoDescription).isEqualTo(defaultLogoDescription)
+        val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+        assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo)
     }
 
     @Test
     @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
     fun logoDescription_setByApp() =
         runGenericTest(logoDescription = logoDescriptionFromApp) {
-            val logoDescription by collectLastValue(kosmos.promptViewModel.logoDescription)
-            assertThat(logoDescription).isEqualTo(logoDescriptionFromApp)
+            val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
+            assertThat(logoInfo!!.second).isEqualTo(logoDescriptionFromApp)
         }
 
     @Test
@@ -1692,7 +1721,7 @@
         logoRes: Int = 0,
         logoBitmap: Bitmap? = null,
         logoDescription: String? = null,
-        packageName: String = OP_PACKAGE_NAME,
+        packageName: String = OP_PACKAGE_NAME_WITH_APP_LOGO,
         block: suspend TestScope.() -> Unit,
     ) {
         val topActivity = ComponentName(packageName, "test app")
@@ -1951,7 +1980,7 @@
     logoResFromApp: Int = 0,
     logoBitmapFromApp: Bitmap? = null,
     logoDescriptionFromApp: String? = null,
-    packageName: String = OP_PACKAGE_NAME,
+    packageName: String = OP_PACKAGE_NAME_WITH_APP_LOGO,
 ) {
     val info =
         PromptInfo().apply {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index 4818119..86da203 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -58,7 +58,6 @@
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.feature.flags.Flags;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -105,8 +104,6 @@
     @Mock
     DozeParameters mDozeParameters;
     @Mock
-    DockManager mDockManager;
-    @Mock
     DevicePostureController mDevicePostureController;
     @Mock
     DozeLog mDozeLog;
@@ -114,8 +111,8 @@
     SystemSettings mSystemSettings;
     @Mock
     DisplayManager mDisplayManager;
-    private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
-    private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
+    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    private final FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
 
     private DozeScreenBrightness mScreen;
 
@@ -249,32 +246,35 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
-    public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Int() {
-        int maxBrightness = 3;
-        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
-                eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness);
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void initialBrightness_clampsToAutoBrightnessValue_Float() {
+        float maxBrightnessFromAutoBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2;
+        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(
+                maxBrightnessFromAutoBrightness
+        );
         when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
                 eq(UserHandle.USER_CURRENT)))
                 .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
-        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
-        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
+        assertEquals(maxBrightnessFromAutoBrightness, mServiceFake.screenBrightnessFloat,
+                DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
-    public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Float() {
-        float maxBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2;
-        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(maxBrightness);
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void initialBrightness_clampsToAutoBrightnessValue_Int() {
+        int maxBrightnessFromAutoBrightness = DEFAULT_BRIGHTNESS_INT / 2;
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightnessFromAutoBrightness);
         when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
                 eq(UserHandle.USER_CURRENT)))
                 .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
-        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
-        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+        assertEquals(maxBrightnessFromAutoBrightness, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
@@ -378,6 +378,54 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void lightSensorChangesInAod_doesNotClampToAutoBrightnessValue_Float() {
+        // GIVEN auto brightness reports low brightness
+        float maxBrightnessFromAutoBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2;
+        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY))
+                .thenReturn(maxBrightnessFromAutoBrightness);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
+                eq(UserHandle.USER_CURRENT)))
+                .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        // GIVEN the device is DOZE_AOD and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is updated
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void lightSensorChangesInAod_doesNotClampToAutoBrightnessValue_Int() {
+        // GIVEN auto brightness reports low brightness
+        int maxBrightnessFromAutoBrightness = 1;
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightnessFromAutoBrightness);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
+                eq(UserHandle.USER_CURRENT)))
+                .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        // GIVEN the device is DOZE_AOD and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is updated
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
+    }
+
+    @Test
     @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
     public void docked_usesLightSensor_Int() {
         // GIVEN the device is docked and the display state changes to ON
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index ff65887..e3a38a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -111,7 +111,6 @@
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeWindowLogger;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -226,7 +225,6 @@
     private @Mock DreamViewModel mDreamViewModel;
     private @Mock CommunalTransitionViewModel mCommunalTransitionViewModel;
     private @Mock SystemPropertiesHelper mSystemPropertiesHelper;
-    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
 
     private FakeFeatureFlags mFeatureFlags;
     private final int mDefaultUserId = 100;
@@ -274,7 +272,7 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                mNotificationShadeWindowModel,
+                mKosmos.getNotificationShadeWindowModel(),
                 mKosmos::getCommunalInteractor);
         mFeatureFlags = new FakeFeatureFlags();
         mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 48a5df9..2af4d87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -45,6 +45,7 @@
 import java.math.RoundingMode
 import java.util.UUID
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.dropWhile
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -332,10 +333,11 @@
         }
 
     @Test
-    fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() {
-        underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
-        assertThat(wtfHandler.failed).isTrue()
-    }
+    fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() =
+        testScope.runTest {
+            underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+            assertThat(wtfHandler.failed).isTrue()
+        }
 
     @Test
     fun attemptToManuallyUpdateTransitionAfterFINISHEDstateThrowsException() =
@@ -414,6 +416,64 @@
                 )
         }
 
+    @Test
+    fun simulateRaceConditionIsProcessedInOrderUsingUpdateTransition() =
+        testScope.runTest {
+            val ktr = KeyguardTransitionRepositoryImpl(kosmos.testDispatcher)
+            val steps by collectValues(ktr.transitions.dropWhile { step -> step.from == OFF })
+
+            // Begin a manual transition
+            val info1 = TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator = null)
+            launch {
+                ktr.forceDelayForRaceConditionTest = false
+                val uuid = ktr.startTransition(info1)
+
+                // Pause here to allow another transition to start
+                delay(20)
+
+                // Attempt to send an update, which should fail
+                ktr.updateTransition(uuid!!, 0.5f, TransitionState.RUNNING)
+            }
+
+            // Now start another transition, which should acquire the preempt the first
+            val info2 = TransitionInfo(OWNER_NAME, LOCKSCREEN, OCCLUDED, animator = null)
+            launch {
+                delay(10)
+                ktr.forceDelayForRaceConditionTest = true
+                ktr.startTransition(info2)
+            }
+
+            runCurrent()
+
+            // Manual transition has started
+            assertThat(steps[0])
+                .isEqualTo(
+                    TransitionStep(info1.from, info1.to, 0f, TransitionState.STARTED, OWNER_NAME)
+                )
+
+            // The second transition has requested to start, and grabbed the mutex. But it is
+            // delayed
+            advanceTimeBy(15L)
+
+            // Advancing another 10ms should now trigger the first transition to request an update,
+            // which should not happen as the second transition has the mutex
+            advanceTimeBy(10L)
+
+            // Finally, advance past the delay in the second transition so it can run
+            advanceTimeBy(50L)
+
+            assertThat(steps[1])
+                .isEqualTo(
+                    TransitionStep(info1.from, info1.to, 0f, TransitionState.CANCELED, OWNER_NAME)
+                )
+            assertThat(steps[2])
+                .isEqualTo(
+                    TransitionStep(info2.from, info2.to, 0f, TransitionState.STARTED, OWNER_NAME)
+                )
+
+            assertThat(steps.size).isEqualTo(3)
+        }
+
     private fun listWithStep(
         step: BigDecimal,
         start: BigDecimal = BigDecimal.ZERO,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
index f884b87..c57aa36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
@@ -29,16 +29,16 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate
 import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.util.mockito.mock
-import junit.framework.Assert.assertEquals
+import kotlin.test.assertEquals
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
+class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
 
     private lateinit var dialog: AlertDialog
 
@@ -115,39 +115,16 @@
         assertEquals(context.getString(resIdFullScreen), secondOptionText)
     }
 
-    @Test
-    fun showDialog_disableSingleApp_hasCastingCapabilities() {
-        setUpAndShowDialog(
-            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
-            hasCastingCapabilities = true
-        )
-
-        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
-        val secondOptionWarningText =
-            spinner.adapter
-                .getDropDownView(1, null, spinner)
-                .findViewById<TextView>(android.R.id.text2)
-                ?.text
-
-        // check that the first option is full screen and enabled
-        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
-
-        // check that the second option is single app and disabled
-        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
-    }
-
     private fun setUpAndShowDialog(
         mediaProjectionConfig: MediaProjectionConfig? = null,
         overrideDisableSingleAppOption: Boolean = false,
-        hasCastingCapabilities: Boolean = false,
     ) {
         val delegate =
-            MediaProjectionPermissionDialogDelegate(
+            ShareToAppPermissionDialogDelegate(
                 context,
                 mediaProjectionConfig,
                 onStartRecordingClicked = {},
                 onCancelClicked = {},
-                hasCastingCapabilities,
                 appName,
                 overrideDisableSingleAppOption,
                 hostUid = 12345,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
similarity index 77%
copy from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
index f884b87..59602dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
@@ -29,23 +29,25 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate
 import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.util.mockito.mock
-import junit.framework.Assert.assertEquals
+import kotlin.test.assertEquals
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
+class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
 
     private lateinit var dialog: AlertDialog
 
     private val appName = "Test App"
 
-    private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
-    private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
+    private val resIdSingleApp =
+        R.string.media_projection_entry_cast_permission_dialog_option_text_single_app
+    private val resIdFullScreen =
+        R.string.media_projection_entry_cast_permission_dialog_option_text_entire_screen
     private val resIdSingleAppDisabled =
         R.string.media_projection_entry_app_permission_dialog_single_app_disabled
 
@@ -98,7 +100,7 @@
     fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() {
         setUpAndShowDialog(
             mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
-            overrideDisableSingleAppOption = true
+            overrideDisableSingleAppOption = true,
         )
 
         val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
@@ -115,43 +117,20 @@
         assertEquals(context.getString(resIdFullScreen), secondOptionText)
     }
 
-    @Test
-    fun showDialog_disableSingleApp_hasCastingCapabilities() {
-        setUpAndShowDialog(
-            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
-            hasCastingCapabilities = true
-        )
-
-        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
-        val secondOptionWarningText =
-            spinner.adapter
-                .getDropDownView(1, null, spinner)
-                .findViewById<TextView>(android.R.id.text2)
-                ?.text
-
-        // check that the first option is full screen and enabled
-        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
-
-        // check that the second option is single app and disabled
-        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
-    }
-
     private fun setUpAndShowDialog(
         mediaProjectionConfig: MediaProjectionConfig? = null,
         overrideDisableSingleAppOption: Boolean = false,
-        hasCastingCapabilities: Boolean = false,
     ) {
         val delegate =
-            MediaProjectionPermissionDialogDelegate(
+            SystemCastPermissionDialogDelegate(
                 context,
                 mediaProjectionConfig,
                 onStartRecordingClicked = {},
                 onCancelClicked = {},
-                hasCastingCapabilities,
                 appName,
                 overrideDisableSingleAppOption,
                 hostUid = 12345,
-                mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
+                mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>(),
             )
 
         dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)
@@ -159,7 +138,7 @@
         SystemUIDialog.setDialogSize(dialog)
 
         dialog.window?.addSystemFlags(
-            WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
+            WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
         )
 
         delegate.onCreate(dialog, savedInstanceState = null)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index e1c3911..b02cccc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
@@ -121,6 +122,9 @@
         Optional<UnfoldTransitionProgressForwarder>
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
 
+    @Mock
+    private lateinit var keyboardTouchpadEduStatsInteractor: KeyboardTouchpadEduStatsInteractor
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -289,7 +293,8 @@
             assistUtils,
             dumpManager,
             unfoldTransitionProgressForwarder,
-            broadcastDispatcher
+            broadcastDispatcher,
+            keyboardTouchpadEduStatsInteractor
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
index 191a048..a5fbfb5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
@@ -18,13 +18,11 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.content.res.Resources
 import android.os.UserHandle
 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 com.android.internal.R
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.screenshot.data.model.DisplayContentModel
 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
@@ -49,7 +47,6 @@
 import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.SHADE_EXPANDED
 import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.WORK_TASK_IS_TOP
 import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.WORK_TASK_NOT_TOP
-import com.android.systemui.util.mockito.whenever
 import com.android.window.flags.Flags
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -58,7 +55,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.MockitoAnnotations
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
@@ -70,19 +66,12 @@
     @JvmField @Rule(order = 2) val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
     @Mock lateinit var mContext: Context
-    @Mock lateinit var mResources: Resources
 
     private val kosmos = Kosmos()
     private lateinit var policy: WorkProfilePolicy
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        // Set desktop mode supported
-        whenever(mContext.resources).thenReturn(mResources)
-        whenever(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true)
-
         policy = WorkProfilePolicy(kosmos.profileTypeRepository, mContext)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/tests/src/com/android/systemui/statusbar/OWNERS
new file mode 100644
index 0000000..1c52b8d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+include /packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
index cd8a740..8f41caf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
@@ -39,7 +40,7 @@
         kosmos.testScope.runTest {
             val latest by collectLastValue(underTest.ongoingCallState)
 
-            val inCall = OngoingCallModel.InCall(startTimeMs = 1000, intent = null)
+            val inCall = inCallModel(startTimeMs = 1000)
             repo.setOngoingCallState(inCall)
             assertThat(latest).isEqualTo(inCall)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 1a6b420..ce79fbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.statusbar.chips.call.ui.viewmodel
 
 import android.app.PendingIntent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
@@ -26,11 +29,13 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
@@ -73,7 +78,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 0, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 0))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
         }
@@ -83,7 +88,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = -2, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = -2))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
         }
@@ -93,7 +98,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 345, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 345))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
         }
@@ -106,7 +111,7 @@
             kosmos.fakeSystemClock.setCurrentTimeMillis(3000)
             kosmos.fakeSystemClock.setElapsedRealtime(400_000)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000))
 
             // The OngoingCallModel start time is relative to currentTimeMillis, so this call
             // started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be
@@ -117,29 +122,97 @@
         }
 
     @Test
-    fun chip_positiveStartTime_iconIsPhone() =
+    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
+            repo.setOngoingCallState(
+                inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>())
+            )
 
-            assertThat(((latest as OngoingActivityChipModel.Shown).icon as Icon.Resource).res)
-                .isEqualTo(com.android.internal.R.drawable.ic_phone)
-            assertThat((latest as OngoingActivityChipModel.Shown).icon!!.contentDescription)
-                .isNotNull()
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
+            assertThat(icon.contentDescription).isNotNull()
         }
 
     @Test
-    fun chip_zeroStartTime_iconIsPhone() =
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 0, intent = null))
+            val notifIcon = mock<StatusBarIconView>()
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon))
 
-            assertThat(((latest as OngoingActivityChipModel.Shown).icon as Icon.Resource).res)
-                .isEqualTo(com.android.internal.R.drawable.ic_phone)
-            assertThat((latest as OngoingActivityChipModel.Shown).icon!!.contentDescription)
-                .isNotNull()
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java)
+            val actualIcon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.StatusBarView)
+                    .impl
+            assertThat(actualIcon).isEqualTo(notifIcon)
+        }
+
+    @Test
+    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            repo.setOngoingCallState(
+                inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>())
+            )
+
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
+            assertThat(icon.contentDescription).isNotNull()
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            val notifIcon = mock<StatusBarIconView>()
+            repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon))
+
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java)
+            val actualIcon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.StatusBarView)
+                    .impl
+            assertThat(actualIcon).isEqualTo(notifIcon)
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = null))
+
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
+            assertThat(icon.contentDescription).isNotNull()
         }
 
     @Test
@@ -147,7 +220,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000))
 
             assertThat((latest as OngoingActivityChipModel.Shown).colors)
                 .isEqualTo(ColorsModel.Themed)
@@ -158,7 +231,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 0, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 0))
 
             assertThat((latest as OngoingActivityChipModel.Shown).colors)
                 .isEqualTo(ColorsModel.Themed)
@@ -172,7 +245,7 @@
             kosmos.fakeSystemClock.setElapsedRealtime(400_000)
 
             // Start a call
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
                 .isEqualTo(398_000)
@@ -186,7 +259,7 @@
             kosmos.fakeSystemClock.setElapsedRealtime(500_000)
 
             // Start a new call, which started 1000ms ago
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 102_000, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 102_000))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
                 .isEqualTo(499_000)
@@ -197,7 +270,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = null))
 
             assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
         }
@@ -208,7 +281,7 @@
             val latest by collectLastValue(underTest.chip)
 
             val intent = mock<PendingIntent>()
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = intent))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = intent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
             assertThat(clickListener).isNotNull()
 
@@ -223,7 +296,7 @@
             val latest by collectLastValue(underTest.chip)
 
             val intent = mock<PendingIntent>()
-            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 0, intent = intent))
+            repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = intent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
             assertThat(clickListener).isNotNull()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 02764f8..a8d2c5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -125,8 +125,11 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
             assertThat((icon.contentDescription as ContentDescription.Resource).res)
                 .isEqualTo(R.string.cast_screen_to_other_device_chip_accessibility_label)
         }
@@ -141,8 +144,11 @@
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
             assertThat((icon.contentDescription as ContentDescription.Resource).res)
                 .isEqualTo(R.string.cast_screen_to_other_device_chip_accessibility_label)
         }
@@ -176,8 +182,11 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
             // This content description is just generic "Casting", not "Casting screen"
             assertThat((icon.contentDescription as ContentDescription.Resource).res)
                 .isEqualTo(R.string.accessibility_casting)
@@ -203,8 +212,11 @@
 
             // Only the projection info will show a timer
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
             // MediaProjection == screen casting, so this content description reflects that we're
             // using the MediaProjection information.
             assertThat((icon.contentDescription as ContentDescription.Resource).res)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index b4a37ee..e68fa0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -148,8 +148,11 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_screenrecord)
             assertThat(icon.contentDescription).isNotNull()
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 2658679..a2ef599 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -133,8 +133,11 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
             assertThat(icon.contentDescription).isNotNull()
         }
 
@@ -147,8 +150,11 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
             assertThat(icon.contentDescription).isNotNull()
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
index b9049e8..a724cfaa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
+import androidx.annotation.DrawableRes
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
@@ -50,7 +51,7 @@
 
             val newChip =
                 OngoingActivityChipModel.Shown.Timer(
-                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    icon = createIcon(R.drawable.ic_cake),
                     colors = ColorsModel.Themed,
                     startTimeMs = 100L,
                     onClickListener = null,
@@ -62,7 +63,7 @@
 
             val newerChip =
                 OngoingActivityChipModel.Shown.IconOnly(
-                    icon = Icon.Resource(R.drawable.ic_hotspot, contentDescription = null),
+                    icon = createIcon(R.drawable.ic_hotspot),
                     colors = ColorsModel.Themed,
                     onClickListener = null,
                 )
@@ -82,7 +83,7 @@
 
             val shownChip =
                 OngoingActivityChipModel.Shown.Timer(
-                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    icon = createIcon(R.drawable.ic_cake),
                     colors = ColorsModel.Themed,
                     startTimeMs = 100L,
                     onClickListener = null,
@@ -122,7 +123,7 @@
 
             val shownChip =
                 OngoingActivityChipModel.Shown.Timer(
-                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    icon = createIcon(R.drawable.ic_cake),
                     colors = ColorsModel.Themed,
                     startTimeMs = 100L,
                     onClickListener = null,
@@ -151,4 +152,7 @@
             advanceTimeBy(2)
             assertThat(latest).isEqualTo(shownChip)
         }
+
+    private fun createIcon(@DrawableRes drawable: Int) =
+        OngoingActivityChipModel.ChipIcon.Basic(Icon.Resource(drawable, contentDescription = null))
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index ee249f0..556ec6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -120,7 +121,7 @@
         testScope.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
 
-            callRepo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34, intent = null))
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
 
             val latest by collectLastValue(underTest.chip)
 
@@ -146,7 +147,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34, intent = null))
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
 
             val latest by collectLastValue(underTest.chip)
 
@@ -160,7 +161,7 @@
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            callRepo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34, intent = null))
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
 
             val latest by collectLastValue(underTest.chip)
 
@@ -171,7 +172,7 @@
     fun chip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
         testScope.runTest {
             // Start with just the lower priority call chip
-            callRepo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34, intent = null))
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
             mediaProjectionState.value = MediaProjectionState.NotProjecting
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
@@ -205,7 +206,7 @@
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
-            callRepo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34, intent = null))
+            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
 
             val latest by collectLastValue(underTest.chip)
 
@@ -335,21 +336,29 @@
 
         fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_screenrecord)
         }
 
         fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
         fun assertIsCallChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res)
-                .isEqualTo(com.android.internal.R.drawable.ic_phone)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.Basic)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index 6a5976e..48ae7a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
@@ -396,9 +397,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.statusBarAppearance)
 
-            ongoingCallRepository.setOngoingCallState(
-                OngoingCallModel.InCall(startTimeMs = 34, intent = null)
-            )
+            ongoingCallRepository.setOngoingCallState(inCallModel(startTimeMs = 34))
             onSystemBarAttributesChanged(
                 requestedVisibleTypes = WindowInsets.Type.navigationBars(),
             )
@@ -411,9 +410,8 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.statusBarAppearance)
 
-            ongoingCallRepository.setOngoingCallState(
-                OngoingCallModel.InCall(startTimeMs = 789, intent = null)
-            )
+            ongoingCallRepository.setOngoingCallState(inCallModel(startTimeMs = 789))
+
             onSystemBarAttributesChanged(
                 requestedVisibleTypes = WindowInsets.Type.statusBars(),
                 appearance = APPEARANCE_OPAQUE_STATUS_BARS,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index bfa816e..25138fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -30,9 +30,12 @@
 import android.os.Bundle
 import android.os.SystemClock
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.InstrumentationRegistry
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -108,6 +111,28 @@
     }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() {
+        val entry =
+            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
+        entry?.let { iconManager.createIcons(it) }
+        testScope.runCurrent()
+
+        assertThat(entry?.icons?.statusBarChipIcon).isNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() {
+        val entry =
+            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
+        entry?.let { iconManager.createIcons(it) }
+        testScope.runCurrent()
+
+        assertThat(entry?.icons?.statusBarChipIcon).isNotNull()
+    }
+
+    @Test
     fun testCreateIcons_importantConversation_shortcutIcon() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
@@ -179,6 +204,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
     fun testCreateIcons_sensitiveImportantConversation() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -187,11 +213,13 @@
         entry?.let { iconManager.createIcons(it) }
         testScope.runCurrent()
         assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+        assertThat(entry?.icons?.statusBarChipIcon?.sourceIcon).isEqualTo(shortcutIc)
         assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
         assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
     }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
     fun testUpdateIcons_sensitiveImportantConversation() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -202,6 +230,7 @@
         entry?.let { iconManager.updateIcons(it) }
         testScope.runCurrent()
         assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+        assertThat(entry?.icons?.statusBarChipIcon?.sourceIcon).isEqualTo(shortcutIc)
         assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
         assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index f9509d2..d1b1f46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -17,6 +17,9 @@
 package com.android.systemui.statusbar.notification.interruption
 
 import android.Manifest.permission
+import android.app.Notification.CATEGORY_ALARM
+import android.app.Notification.CATEGORY_CAR_EMERGENCY
+import android.app.Notification.CATEGORY_CAR_WARNING
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.NotificationManager
@@ -256,6 +259,61 @@
     }
 
     @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryAlarm() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_ALARM
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryCarEmergency() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_CAR_EMERGENCY
+
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryCarWarning() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_CAR_WARNING
+                }
+            )
+        }
+    }
+
+    @Test
     fun testAvalancheFilter_duringAvalanche_allowFsi() {
         avalancheProvider.startTime = whenAgo(10)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 9b61105..af5e60e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mCentralSurfaces).hideKeyguard();
         verify(mPrimaryBouncerInteractor).show(true);
     }
@@ -1084,7 +1084,7 @@
                 .thenReturn(KeyguardState.LOCKSCREEN);
 
         reset(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mPrimaryBouncerInteractor).show(true);
         verify(mCentralSurfaces).showKeyguard();
     }
@@ -1092,26 +1092,11 @@
     @Test
     @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
-        boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mCentralSurfaces, never()).hideKeyguard();
-        verify(mPrimaryBouncerInteractor).show(true);
-    }
-
-    @Test
-    @DisableSceneContainer
-    public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
-        boolean isFalsingReset = true;
-        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
-                KeyguardSecurityModel.SecurityMode.SimPin);
-        when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
-        verify(mCentralSurfaces, never()).hideKeyguard();
-
-        // Do not refresh the full screen bouncer if the call is from falsing
         verify(mPrimaryBouncerInteractor, never()).show(true);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
index 6c2e2c6..dfe01bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
@@ -28,7 +28,7 @@
 import android.widget.LinearLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
 import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
 import com.android.systemui.SysuiTestCase
@@ -39,6 +39,7 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -160,6 +161,47 @@
         }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() =
+        testScope.runTest {
+            val icon = mock<StatusBarIconView>()
+            setNotifOnRepo(
+                activeNotificationModel(
+                    key = "ongoingNotif",
+                    callType = CallType.Ongoing,
+                    uid = CALL_UID,
+                    statusBarChipIcon = icon,
+                    whenTime = 567,
+                )
+            )
+
+            val repoState = ongoingCallRepository.ongoingCallState.value
+            assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
+            assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull()
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+    fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() =
+        testScope.runTest {
+            val icon = mock<StatusBarIconView>()
+
+            setNotifOnRepo(
+                activeNotificationModel(
+                    key = "ongoingNotif",
+                    callType = CallType.Ongoing,
+                    uid = CALL_UID,
+                    statusBarChipIcon = icon,
+                    whenTime = 567,
+                )
+            )
+
+            val repoState = ongoingCallRepository.ongoingCallState.value
+            assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
+            assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isEqualTo(icon)
+        }
+
+    @Test
     fun notifRepoHasOngoingCallNotif_isOngoingCallNotif_windowControllerUpdated() {
         setCallNotifOnRepo()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
index cbb8fe8..4c6eaa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -33,7 +34,7 @@
 
     @Test
     fun hasOngoingCall_matchesSet() {
-        val inCallModel = OngoingCallModel.InCall(startTimeMs = 654, intent = null)
+        val inCallModel = inCallModel(startTimeMs = 654)
         underTest.setOngoingCallState(inCallModel)
 
         assertThat(underTest.ongoingCallState.value).isEqualTo(inCallModel)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
new file mode 100644
index 0000000..6aefbe9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
+import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeGestureMonitorTest : SysuiTestCase() {
+
+    private var gestureState = NOT_STARTED
+    private val gestureMonitor =
+        HomeGestureMonitor(
+            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+            gestureStateChangedCallback = { gestureState = it }
+        )
+
+    @Test
+    fun triggersGestureFinishedForThreeFingerGestureUp() {
+        assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = FINISHED)
+    }
+
+    @Test
+    fun triggersGestureProgressForThreeFingerGestureStarted() {
+        assertStateAfterEvents(
+            events = ThreeFingerGesture.startEvents(x = 0f, y = 0f),
+            expectedState = IN_PROGRESS
+        )
+    }
+
+    @Test
+    fun doesntTriggerGestureFinished_onGestureDistanceTooShort() {
+        assertStateAfterEvents(
+            events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2),
+            expectedState = NOT_STARTED
+        )
+    }
+
+    @Test
+    fun doesntTriggerGestureFinished_onThreeFingersSwipeInOtherDirections() {
+        assertStateAfterEvents(events = ThreeFingerGesture.swipeDown(), expectedState = NOT_STARTED)
+        assertStateAfterEvents(events = ThreeFingerGesture.swipeLeft(), expectedState = NOT_STARTED)
+        assertStateAfterEvents(
+            events = ThreeFingerGesture.swipeRight(),
+            expectedState = NOT_STARTED
+        )
+    }
+
+    @Test
+    fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
+        assertStateAfterEvents(events = TwoFingerGesture.swipeUp(), expectedState = NOT_STARTED)
+    }
+
+    @Test
+    fun doesntTriggerGestureFinished_onFourFingersSwipe() {
+        assertStateAfterEvents(events = FourFingerGesture.swipeUp(), expectedState = NOT_STARTED)
+    }
+
+    private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
+        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        assertThat(gestureState).isEqualTo(expectedState)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7f964d1..e5e04dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -349,10 +349,10 @@
     private Display mDefaultDisplay;
     @Mock
     private Lazy<ViewCapture> mLazyViewCapture;
-    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
 
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     private ShadeInteractor mShadeInteractor;
+    private NotificationShadeWindowModel mNotificationShadeWindowModel;
     private ShellTaskOrganizer mShellTaskOrganizer;
     private TaskViewTransitions mTaskViewTransitions;
 
@@ -411,6 +411,7 @@
         when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));
 
         mShadeInteractor = mKosmos.getShadeInteractor();
+        mNotificationShadeWindowModel = mKosmos.getNotificationShadeWindowModel();
 
         mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
                 mContext,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
index bade91a..3816e1b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.education.data.repository
 
+import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.education.data.model.GestureEduModel
-import com.android.systemui.shared.education.GestureType
 import java.time.Clock
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index b5ea619..616f2b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -283,7 +283,7 @@
         )
     }
 
-    override fun updateTransition(
+    override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 8614fc9..e6bd24b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -59,6 +59,7 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shadeController
+import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -140,6 +141,7 @@
     val shadeController by lazy { kosmos.shadeController }
     val shadeRepository by lazy { kosmos.shadeRepository }
     val shadeInteractor by lazy { kosmos.shadeInteractor }
+    val notificationShadeWindowModel by lazy { kosmos.notificationShadeWindowModel }
     val wifiInteractor by lazy { kosmos.wifiInteractor }
     val fakeWifiRepository by lazy { kosmos.fakeWifiRepository }
     val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 37f1f13..76bdc0d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -18,6 +18,7 @@
 
 import android.app.PendingIntent
 import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
@@ -36,6 +37,7 @@
     aodIcon: Icon? = null,
     shelfIcon: Icon? = null,
     statusBarIcon: Icon? = null,
+    statusBarChipIcon: StatusBarIconView? = null,
     uid: Int = 0,
     instanceId: Int? = null,
     isGroupSummary: Boolean = false,
@@ -57,6 +59,7 @@
         aodIcon = aodIcon,
         shelfIcon = shelfIcon,
         statusBarIcon = statusBarIcon,
+        statusBarChipIconView = statusBarChipIcon,
         uid = uid,
         packageName = packageName,
         contentIntent = contentIntent,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
new file mode 100644
index 0000000..3963d7c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.phone.ongoingcall.shared.model
+
+import android.app.PendingIntent
+import com.android.systemui.statusbar.StatusBarIconView
+
+/** Helper for building [OngoingCallModel.InCall] instances in tests. */
+fun inCallModel(
+    startTimeMs: Long,
+    notificationIcon: StatusBarIconView? = null,
+    intent: PendingIntent? = null
+) = OngoingCallModel.InCall(startTimeMs, notificationIcon, intent)
diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
index cbbce1a..8663593 100644
--- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
+++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
@@ -120,6 +120,14 @@
     static final String SYSTEM_GENERATION = "system_gen";
     static final String LOCK_GENERATION = "lock_gen";
 
+    static final String DEVICE_CONFIG_WIDTH = "device_config_width";
+
+    static final String DEVICE_CONFIG_HEIGHT = "device_config_height";
+
+    static final String DEVICE_CONFIG_SECONDARY_WIDTH = "device_config_secondary_width";
+
+    static final String DEVICE_CONFIG_SECONDARY_HEIGHT = "device_config_secondary_height";
+
     static final float DEFAULT_ACCEPTABLE_PARALLAX = 0.2f;
 
     // If this file exists, it means we exceeded our quota last time
@@ -175,6 +183,16 @@
             // disk churn.
             final int lastSysGeneration = sharedPrefs.getInt(SYSTEM_GENERATION, /* defValue= */ -1);
             final int lastLockGeneration = sharedPrefs.getInt(LOCK_GENERATION, /* defValue= */ -1);
+
+            final int deviceConfigWidth = sharedPrefs.getInt(
+                    DEVICE_CONFIG_WIDTH, /* defValue= */ -1);
+            final int deviceConfigHeight = sharedPrefs.getInt(
+                    DEVICE_CONFIG_HEIGHT, /* defValue= */ -1);
+            final int deviceConfigSecondaryWidth = sharedPrefs.getInt(
+                    DEVICE_CONFIG_SECONDARY_WIDTH, /* defValue= */ -1);
+            final int deviceConfigSecondaryHeight = sharedPrefs.getInt(
+                    DEVICE_CONFIG_SECONDARY_HEIGHT, /* defValue= */ -1);
+
             final int sysGeneration = mWallpaperManager.getWallpaperId(FLAG_SYSTEM);
             final int lockGeneration = mWallpaperManager.getWallpaperId(FLAG_LOCK);
             final boolean sysChanged = (sysGeneration != lastSysGeneration);
@@ -195,7 +213,11 @@
             backupWallpaperInfoFile(/* sysOrLockChanged= */ sysChanged || lockChanged, data);
             backupSystemWallpaperFile(sharedPrefs, sysChanged, sysGeneration, data);
             backupLockWallpaperFileIfItExists(sharedPrefs, lockChanged, lockGeneration, data);
-            backupDeviceInfoFile(data);
+
+            final boolean isDeviceConfigChanged = isDeviceConfigChanged(deviceConfigWidth,
+                    deviceConfigHeight, deviceConfigSecondaryWidth, deviceConfigSecondaryHeight);
+
+            backupDeviceInfoFile(sharedPrefs, isDeviceConfigChanged, data);
         } catch (Exception e) {
             Slog.e(TAG, "Unable to back up wallpaper", e);
             mEventLogger.onBackupException(e);
@@ -209,50 +231,72 @@
         }
     }
 
+    private boolean isDeviceConfigChanged(int width, int height, int secondaryWidth,
+            int secondaryHeight) {
+        Point currentDimensions = getScreenDimensions();
+        Display smallerDisplay = getSmallerDisplayIfExists();
+        Point currentSecondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
+                new Point(0, 0);
+
+        return (currentDimensions.x != width
+                || currentDimensions.y != height
+                || currentSecondaryDimensions.x != secondaryWidth
+                || currentSecondaryDimensions.y != secondaryHeight);
+    }
+
     /**
      * This method backs up the device dimension information. The device data will always get
      * overwritten when triggering a backup
      */
-    private void backupDeviceInfoFile(FullBackupDataOutput data)
+    private void backupDeviceInfoFile(SharedPreferences sharedPrefs, boolean isDeviceConfigChanged,
+            FullBackupDataOutput data)
             throws IOException {
         final File deviceInfoStage = new File(getFilesDir(), WALLPAPER_BACKUP_DEVICE_INFO_STAGE);
 
-        // save the dimensions of the device with xml formatting
-        Point dimensions = getScreenDimensions();
-        Display smallerDisplay = getSmallerDisplayIfExists();
-        Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
-                new Point(0, 0);
+        if (isDeviceConfigChanged) {
+            // save the dimensions of the device with xml formatting
+            Point dimensions = getScreenDimensions();
+            Display smallerDisplay = getSmallerDisplayIfExists();
+            Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
+                    new Point(0, 0);
 
-        deviceInfoStage.createNewFile();
-        FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false);
-        TypedXmlSerializer out = Xml.resolveSerializer(fstream);
-        out.startDocument(null, true);
-        out.startTag(null, "dimensions");
+            deviceInfoStage.createNewFile();
+            FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false);
+            TypedXmlSerializer out = Xml.resolveSerializer(fstream);
+            out.startDocument(null, true);
+            out.startTag(null, "dimensions");
 
-        out.startTag(null, "width");
-        out.text(String.valueOf(dimensions.x));
-        out.endTag(null, "width");
+            out.startTag(null, "width");
+            out.text(String.valueOf(dimensions.x));
+            out.endTag(null, "width");
 
-        out.startTag(null, "height");
-        out.text(String.valueOf(dimensions.y));
-        out.endTag(null, "height");
+            out.startTag(null, "height");
+            out.text(String.valueOf(dimensions.y));
+            out.endTag(null, "height");
 
-        if (smallerDisplay != null) {
-            out.startTag(null, "secondarywidth");
-            out.text(String.valueOf(secondaryDimensions.x));
-            out.endTag(null, "secondarywidth");
+            if (smallerDisplay != null) {
+                out.startTag(null, "secondarywidth");
+                out.text(String.valueOf(secondaryDimensions.x));
+                out.endTag(null, "secondarywidth");
 
-            out.startTag(null, "secondaryheight");
-            out.text(String.valueOf(secondaryDimensions.y));
-            out.endTag(null, "secondaryheight");
+                out.startTag(null, "secondaryheight");
+                out.text(String.valueOf(secondaryDimensions.y));
+                out.endTag(null, "secondaryheight");
+            }
+
+            out.endTag(null, "dimensions");
+            out.endDocument();
+            fstream.flush();
+            FileUtils.sync(fstream);
+            fstream.close();
+
+            SharedPreferences.Editor editor = sharedPrefs.edit();
+            editor.putInt(DEVICE_CONFIG_WIDTH, dimensions.x);
+            editor.putInt(DEVICE_CONFIG_HEIGHT, dimensions.y);
+            editor.putInt(DEVICE_CONFIG_SECONDARY_WIDTH, secondaryDimensions.x);
+            editor.putInt(DEVICE_CONFIG_SECONDARY_HEIGHT, secondaryDimensions.y);
+            editor.apply();
         }
-
-        out.endTag(null, "dimensions");
-        out.endDocument();
-        fstream.flush();
-        FileUtils.sync(fstream);
-        fstream.close();
-
         if (DEBUG) Slog.v(TAG, "Storing device dimension data");
         backupFile(deviceInfoStage, data);
     }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
index ee28099..0238baa 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
@@ -16,6 +16,7 @@
 package com.android.ravenwood.common;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 
 /**
  * Collection of methods to workaround limitation in the hostside JVM.
@@ -44,6 +45,11 @@
     public abstract int getFdInt(FileDescriptor fd);
 
     /**
+     * Equivalent to Android's Os.close(fd).
+     */
+    public abstract void closeFd(FileDescriptor fd) throws IOException;
+
+    /**
      * Placeholder implementation for the host side.
      *
      * Even on the host side, we don't want to throw just because the class is loaded,
@@ -64,5 +70,10 @@
         public int getFdInt(FileDescriptor fd) {
             throw calledOnHostside();
         }
+
+        @Override
+        public void closeFd(FileDescriptor fd) {
+            throw calledOnHostside();
+        }
     }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
index 9aedaab..a260147 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
@@ -16,6 +16,8 @@
 package com.android.ravenwood.common;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 
 class OpenJdkWorkaround extends JvmWorkaround {
     @Override
@@ -43,4 +45,19 @@
                     + " perhaps JRE has changed?", e);
         }
     }
+
+    @Override
+    public void closeFd(FileDescriptor fd) throws IOException {
+        try {
+            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
+                    "getJavaIOFileDescriptorAccess").invoke(null);
+            Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
+                    "close", FileDescriptor.class).invoke(obj, fd);
+        } catch (InvocationTargetException e) {
+            SneakyThrow.sneakyThrow(e.getTargetException());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
+                    + " perhaps JRE has changed?", e);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java
similarity index 60%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
copy to ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java
index 14b9e7f..0dbf7df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java
@@ -13,15 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.wm.shell.windowdecor.common
+package com.android.ravenwood.common;
 
-/** A callback to be invoked when a Task's window decor element is clicked. */
-fun interface OnTaskActionClickListener {
+public class SneakyThrow {
+
+    private SneakyThrow() {
+    }
+
     /**
-     * Called when a task's decor element has been clicked.
-     *
-     * @param taskId the id of the task.
-     * @param tag a readable identifier for the element.
+     * Throw checked exceptions without the need to declare in method signature
      */
-    fun onClick(taskId: Int, tag: String)
+    public static void sneakyThrow(Throwable t) {
+        SneakyThrow.<RuntimeException>sneakyThrow_(t);
+    }
+
+    private static <T extends Throwable> void sneakyThrow_(Throwable t) throws T {
+        throw (T) t;
+    }
 }
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 1a15d7a..5a3589d 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -16,105 +16,16 @@
 
 package com.android.platform.test.ravenwood.nativesubstitution;
 
-import static android.os.ParcelFileDescriptor.MODE_APPEND;
-import static android.os.ParcelFileDescriptor.MODE_CREATE;
-import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
-import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
-import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
-import static android.os.ParcelFileDescriptor.MODE_WORLD_READABLE;
-import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE;
-import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
 import com.android.ravenwood.common.JvmWorkaround;
 
-import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.HashMap;
-import java.util.Map;
 
 public class ParcelFileDescriptor_host {
-    private static final String TAG = "ParcelFileDescriptor_host";
-
-    /**
-     * Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference
-     * alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This
-     * gives us a way to look up the original parent object when closing later.
-     */
-    @GuardedBy("sActive")
-    private static final Map<FileDescriptor, RandomAccessFile> sActive = new HashMap<>();
-
-    public static void native_setFdInt$ravenwood(FileDescriptor fd, int fdInt) {
+    public static void setFdInt(FileDescriptor fd, int fdInt) {
         JvmWorkaround.getInstance().setFdInt(fd, fdInt);
     }
 
-    public static int native_getFdInt$ravenwood(FileDescriptor fd) {
+    public static int getFdInt(FileDescriptor fd) {
         return JvmWorkaround.getInstance().getFdInt(fd);
     }
-
-    public static FileDescriptor native_open$ravenwood(File file, int pfdMode) throws IOException {
-        if ((pfdMode & MODE_CREATE) != 0 && !file.exists()) {
-            throw new FileNotFoundException();
-        }
-
-        final String modeString;
-        if ((pfdMode & MODE_READ_WRITE) == MODE_READ_WRITE) {
-            modeString = "rw";
-        } else if ((pfdMode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
-            modeString = "rw";
-        } else if ((pfdMode & MODE_READ_ONLY) == MODE_READ_ONLY) {
-            modeString = "r";
-        } else {
-            throw new IllegalArgumentException();
-        }
-
-        final RandomAccessFile raf = new RandomAccessFile(file, modeString);
-
-        // Now that we have a real file on disk, match requested flags
-        if ((pfdMode & MODE_TRUNCATE) != 0) {
-            raf.setLength(0);
-        }
-        if ((pfdMode & MODE_APPEND) != 0) {
-            raf.seek(raf.length());
-        }
-        if ((pfdMode & MODE_WORLD_READABLE) != 0) {
-            file.setReadable(true, false);
-        }
-        if ((pfdMode & MODE_WORLD_WRITEABLE) != 0) {
-            file.setWritable(true, false);
-        }
-
-        final FileDescriptor fd = raf.getFD();
-        synchronized (sActive) {
-            sActive.put(fd, raf);
-        }
-        return fd;
-    }
-
-    public static void native_close$ravenwood(FileDescriptor fd) {
-        final RandomAccessFile raf;
-        synchronized (sActive) {
-            raf = sActive.remove(fd);
-        }
-        int fdInt = JvmWorkaround.getInstance().getFdInt(fd);
-        try {
-            if (raf != null) {
-                raf.close();
-            } else {
-                // This FD wasn't created by native_open$ravenwood().
-                // The FD was passed to the PFD ctor. Just close it.
-                Os.close(fd);
-            }
-        } catch (IOException | ErrnoException e) {
-            Log.w(TAG, "Exception thrown while closing fd " + fdInt, e);
-        }
-    }
 }
-;
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 825ab72..ecaa816 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -15,9 +15,11 @@
  */
 package android.system;
 
+import com.android.ravenwood.common.JvmWorkaround;
 import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 
 /**
  * OS class replacement used on Ravenwood. For now, we just implement APIs as we need them...
@@ -56,6 +58,15 @@
 
     /** Ravenwood version of the OS API. */
     public static void close(FileDescriptor fd) throws ErrnoException {
-        RavenwoodRuntimeNative.close(fd);
+        try {
+            JvmWorkaround.getInstance().closeFd(fd);
+        } catch (IOException e) {
+            // The only valid error on Linux that can happen is EIO
+            throw new ErrnoException("close", OsConstants.EIO);
+        }
+    }
+
+    public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
+        return RavenwoodRuntimeNative.open(path, flags, mode);
     }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index 2bc8e71..beba833 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -48,7 +48,7 @@
 
     public static native StructStat stat(String path) throws ErrnoException;
 
-    private static native void nClose(int fd) throws ErrnoException;
+    private static native int nOpen(String path, int flags, int mode) throws ErrnoException;
 
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
@@ -69,7 +69,7 @@
     public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
         var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd));
 
-        var retFd = new java.io.FileDescriptor();
+        var retFd = new FileDescriptor();
         JvmWorkaround.getInstance().setFdInt(retFd, fdInt);
         return retFd;
     }
@@ -86,10 +86,11 @@
         return nFstat(fdInt);
     }
 
-    /** See close(2) */
-    public static void close(FileDescriptor fd) throws ErrnoException {
-        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
-
-        nClose(fdInt);
+    public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
+        int fd = nOpen(path, flags, mode);
+        if (fd < 0) return null;
+        var retFd = new FileDescriptor();
+        JvmWorkaround.getInstance().setFdInt(retFd, fd);
+        return retFd;
     }
 }
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index ee84954..c804928 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -18,9 +18,11 @@
 #include <sys/stat.h>
 #include <string.h>
 #include <unistd.h>
+#include <string>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedLocalRef.h>
 #include <nativehelper/ScopedUtfChars.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
 
 #include "jni.h"
 #include "utils/Log.h"
@@ -49,6 +51,43 @@
 static jclass g_StructStat;
 static jclass g_StructTimespecClass;
 
+// We have to explicitly decode the string to real UTF-8, because when using GetStringUTFChars
+// we only get modified UTF-8, which is not the platform string type used in host JVM.
+struct ScopedRealUtf8Chars {
+    ScopedRealUtf8Chars(JNIEnv* env, jstring s) : valid_(false) {
+        if (s == nullptr) {
+            jniThrowNullPointerException(env);
+            return;
+        }
+        jclass clazz = env->GetObjectClass(s);
+        jmethodID getBytes = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
+
+        ScopedLocalRef<jstring> utf8(env, env->NewStringUTF("UTF-8"));
+        ScopedLocalRef<jbyteArray> jbytes(env,
+            (jbyteArray) env->CallObjectMethod(s, getBytes, utf8.get()));
+
+        ScopedByteArrayRO bytes(env, jbytes.get());
+        string_.append((const char *) bytes.get(), bytes.size());
+        valid_ = true;
+    }
+
+    const char* c_str() const {
+        return valid_ ? string_.c_str() : nullptr;
+    }
+
+    size_t size() const {
+        return string_.size();
+    }
+
+    const char& operator[](size_t n) const {
+        return string_[n];
+    }
+
+private:
+    std::string string_;
+    bool valid_;
+};
+
 static jclass findClass(JNIEnv* env, const char* name) {
     ScopedLocalRef<jclass> localClass(env, env->FindClass(name));
     jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
@@ -99,7 +138,7 @@
 }
 
 static jobject doStat(JNIEnv* env, jstring javaPath, bool isLstat) {
-    ScopedUtfChars path(env, javaPath);
+    ScopedRealUtf8Chars path(env, javaPath);
     if (path.c_str() == NULL) {
         return NULL;
     }
@@ -167,9 +206,12 @@
     return doStat(env, javaPath, false);
 }
 
-static void nClose(JNIEnv* env, jclass, jint fd) {
-    // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129
-    throwIfMinusOne(env, "close", close(fd));
+static jint Linux_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint mode) {
+    ScopedRealUtf8Chars path(env, javaPath);
+    if (path.c_str() == NULL) {
+        return -1;
+    }
+    return throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
 }
 
 // ---- Registration ----
@@ -184,7 +226,7 @@
     { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
     { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
     { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
-    { "nClose", "(I)V", (void*)nClose },
+    { "nOpen", "(Ljava/lang/String;II)I", (void*)Linux_open },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index b5b998f..6b6b39d 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -1131,7 +1131,10 @@
             }
 
             if (isAlwaysOnMagnificationEnabled()) {
-                zoomOutFromService(displayId);
+                if (!mControllerCtx.getContext().getResources().getBoolean(
+                        R.bool.config_magnification_keep_zoom_level_when_context_changed)) {
+                    zoomOutFromService(displayId);
+                }
             } else {
                 reset(displayId, true);
             }
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 988a213..84b5c39 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -28,7 +28,6 @@
 import android.app.WindowConfiguration;
 import android.app.compat.CompatChanges;
 import android.companion.virtual.VirtualDeviceManager.ActivityListener;
-import android.companion.virtual.flags.Flags;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.AttributionSource;
@@ -108,12 +107,11 @@
     private boolean mActivityLaunchAllowedByDefault;
     @NonNull
     @GuardedBy("mGenericWindowPolicyControllerLock")
-    private final Set<ComponentName> mActivityPolicyExemptions;
+    private final ArraySet<ComponentName> mActivityPolicyExemptions;
     private final boolean mCrossTaskNavigationAllowedByDefault;
     @NonNull
     private final ArraySet<ComponentName> mCrossTaskNavigationExemptions;
     @Nullable
-    private final ComponentName mPermissionDialogComponent;
     private final Object mGenericWindowPolicyControllerLock = new Object();
     @Nullable private final ActivityBlockedCallback mActivityBlockedCallback;
 
@@ -178,7 +176,6 @@
             @NonNull Set<ComponentName> activityPolicyExemptions,
             boolean crossTaskNavigationAllowedByDefault,
             @NonNull Set<ComponentName> crossTaskNavigationExemptions,
-            @Nullable ComponentName permissionDialogComponent,
             @Nullable ActivityListener activityListener,
             @Nullable ActivityBlockedCallback activityBlockedCallback,
             @Nullable SecureWindowCallback secureWindowCallback,
@@ -190,10 +187,9 @@
         mAttributionSource = attributionSource;
         mAllowedUsers = allowedUsers;
         mActivityLaunchAllowedByDefault = activityLaunchAllowedByDefault;
-        mActivityPolicyExemptions = activityPolicyExemptions;
+        mActivityPolicyExemptions = new ArraySet<>(activityPolicyExemptions);
         mCrossTaskNavigationAllowedByDefault = crossTaskNavigationAllowedByDefault;
         mCrossTaskNavigationExemptions = new ArraySet<>(crossTaskNavigationExemptions);
-        mPermissionDialogComponent = permissionDialogComponent;
         mActivityBlockedCallback = activityBlockedCallback;
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
@@ -287,28 +283,15 @@
     public boolean canActivityBeLaunched(@NonNull ActivityInfo activityInfo,
             @Nullable Intent intent, @WindowConfiguration.WindowingMode int windowingMode,
             int launchingFromDisplayId, boolean isNewTask) {
-        if (Flags.interceptIntentsBeforeApplyingPolicy()) {
-            if (mIntentListenerCallback != null && intent != null
-                    && mIntentListenerCallback.shouldInterceptIntent(intent)) {
-                logActivityLaunchBlocked("Virtual device intercepting intent");
-                return false;
-            }
-            if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId,
-                    isNewTask)) {
-                notifyActivityBlocked(activityInfo);
-                return false;
-            }
-        } else {
-            if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId,
-                    isNewTask)) {
-                notifyActivityBlocked(activityInfo);
-                return false;
-            }
-            if (mIntentListenerCallback != null && intent != null
-                    && mIntentListenerCallback.shouldInterceptIntent(intent)) {
-                logActivityLaunchBlocked("Virtual device intercepting intent");
-                return false;
-            }
+        if (mIntentListenerCallback != null && intent != null
+                && mIntentListenerCallback.shouldInterceptIntent(intent)) {
+            logActivityLaunchBlocked("Virtual device intercepting intent");
+            return false;
+        }
+        if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId,
+                isNewTask)) {
+            notifyActivityBlocked(activityInfo);
+            return false;
         }
         return true;
     }
@@ -370,14 +353,6 @@
             return false;
         }
 
-        // mPermissionDialogComponent being null means we don't want to block permission Dialogs
-        // based on FLAG_STREAM_PERMISSIONS
-        if (mPermissionDialogComponent != null
-                && mPermissionDialogComponent.equals(activityComponent)) {
-            logActivityLaunchBlocked("Permission dialog not allowed on virtual device");
-            return false;
-        }
-
         return true;
     }
 
@@ -487,11 +462,9 @@
                 && displayId != INVALID_DISPLAY) {
             mActivityBlockedCallback.onActivityBlocked(displayId, activityInfo);
         }
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            Counter.logIncrementWithUid(
-                    "virtual_devices.value_activity_blocked_count",
-                    mAttributionSource.getUid());
-        }
+        Counter.logIncrementWithUid(
+                "virtual_devices.value_activity_blocked_count",
+                mAttributionSource.getUid());
     }
 
     private static boolean isAllowedByPolicy(boolean allowedByDefault,
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index d091ce8..8da58cf 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -842,11 +842,9 @@
                             deviceName, inputDeviceId));
         }
 
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            String metricId = getMetricIdForInputType(type);
-            if (metricId != null) {
-                Counter.logIncrementWithUid(metricId, mAttributionSource.getUid());
-            }
+        String metricId = getMetricIdForInputType(type);
+        if (metricId != null) {
+            Counter.logIncrementWithUid(metricId, mAttributionSource.getUid());
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/SensorController.java b/services/companion/java/com/android/server/companion/virtual/SensorController.java
index 0655685..8d075db 100644
--- a/services/companion/java/com/android/server/companion/virtual/SensorController.java
+++ b/services/companion/java/com/android/server/companion/virtual/SensorController.java
@@ -145,11 +145,9 @@
             mSensorDescriptors.put(sensorToken, sensorDescriptor);
             mVirtualSensors.put(handle, sensor);
         }
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            Counter.logIncrementWithUid(
-                    "virtual_devices.value_virtual_sensors_created_count",
-                    mAttributionSource.getUid());
-        }
+        Counter.logIncrementWithUid(
+                "virtual_devices.value_virtual_sensors_created_count",
+                mAttributionSource.getUid());
     }
 
     boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index ee7d0ae..ed2c90d 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -29,8 +29,6 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
-import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix;
-import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -208,7 +206,6 @@
     @GuardedBy("mVirtualDeviceLock")
     @NonNull
     private final Set<ComponentName> mActivityPolicyExemptions;
-    private final ComponentName mPermissionDialogComponent;
 
     private ActivityListener createListenerAdapter() {
         return new ActivityListener() {
@@ -343,11 +340,6 @@
         if (mCameraAccessController != null) {
             mCameraAccessController.startObservingIfNeeded();
         }
-        if (!Flags.streamPermissions()) {
-            mPermissionDialogComponent = getPermissionDialogComponent();
-        } else {
-            mPermissionDialogComponent = null;
-        }
         mVirtualCameraController = virtualCameraController;
         try {
             token.linkToDeath(this, 0);
@@ -558,6 +550,36 @@
         }
     }
 
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void addActivityPolicyExemptionForDisplay(
+            int displayId, @NonNull ComponentName componentName) {
+        super.addActivityPolicyExemptionForDisplay_enforcePermission();
+        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+            return;
+        }
+        synchronized (mVirtualDeviceLock) {
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
+            mVirtualDisplays.get(displayId).getWindowPolicyController()
+                    .addActivityPolicyExemption(componentName);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void removeActivityPolicyExemptionForDisplay(
+            int displayId, @NonNull ComponentName componentName) {
+        super.removeActivityPolicyExemptionForDisplay_enforcePermission();
+        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+            return;
+        }
+        synchronized (mVirtualDeviceLock) {
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
+            mVirtualDisplays.get(displayId).getWindowPolicyController()
+                    .removeActivityPolicyExemption(componentName);
+        }
+    }
+
     private void sendPendingIntent(int displayId, PendingIntent pendingIntent)
             throws PendingIntent.CanceledException {
         final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
@@ -657,12 +679,7 @@
             @Nullable IAudioConfigChangedCallback configChangedCallback) {
         super.onAudioSessionStarting_enforcePermission();
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplays.contains(displayId)) {
-                throw new SecurityException(
-                        "Cannot start audio session for a display not associated with this virtual "
-                                + "device");
-            }
-
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
             if (mVirtualAudioController == null) {
                 mVirtualAudioController = new VirtualAudioController(mContext, mAttributionSource);
                 GenericWindowPolicyController gwpc = mVirtualDisplays.get(
@@ -706,6 +723,9 @@
                 break;
             case POLICY_TYPE_ACTIVITY:
                 synchronized (mVirtualDeviceLock) {
+                    if (getDevicePolicy(policyType) != devicePolicy) {
+                        mActivityPolicyExemptions.clear();
+                    }
                     mDevicePolicies.put(policyType, devicePolicy);
                     for (int i = 0; i < mVirtualDisplays.size(); i++) {
                         mVirtualDisplays.valueAt(i).getWindowPolicyController()
@@ -736,6 +756,33 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void setDevicePolicyForDisplay(int displayId,
+            @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
+            @VirtualDeviceParams.DevicePolicy int devicePolicy) {
+        super.setDevicePolicyForDisplay_enforcePermission();
+        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+            return;
+        }
+        synchronized (mVirtualDeviceLock) {
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
+            switch (policyType) {
+                case POLICY_TYPE_RECENTS:
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+                    break;
+                case POLICY_TYPE_ACTIVITY:
+                    mVirtualDisplays.get(displayId).getWindowPolicyController()
+                            .setActivityLaunchDefaultAllowed(devicePolicy == DEVICE_POLICY_DEFAULT);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Device policy " + policyType
+                            + " cannot be changed for a specific display. ");
+            }
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
         super.createVirtualDpad_enforcePermission();
         Objects.requireNonNull(config);
@@ -1243,7 +1290,6 @@
                 /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
                         ? mParams.getBlockedCrossTaskNavigations()
                         : mParams.getAllowedCrossTaskNavigations(),
-                mPermissionDialogComponent,
                 mActivityListenerAdapter,
                 this::onActivityBlocked,
                 this::onSecureWindowShown,
@@ -1255,13 +1301,6 @@
         return gwpc;
     }
 
-    private ComponentName getPermissionDialogComponent() {
-        Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
-        PackageManager packageManager = mContext.getPackageManager();
-        intent.setPackage(packageManager.getPermissionControllerPackageName());
-        return intent.resolveActivity(packageManager);
-    }
-
     int createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
             @NonNull IVirtualDisplayCallback callback, String packageName) {
         GenericWindowPolicyController gwpc;
@@ -1273,7 +1312,7 @@
                 this, gwpc, packageName);
         gwpc.setDisplayId(displayId, /* isMirrorDisplay= */ Flags.interactiveScreenMirror()
                 && mDisplayManagerInternal.getDisplayIdToMirror(displayId)
-                    != Display.INVALID_DISPLAY);
+                != Display.INVALID_DISPLAY);
 
         boolean showPointer;
         synchronized (mVirtualDeviceLock) {
@@ -1304,11 +1343,9 @@
             Binder.restoreCallingIdentity(token);
         }
 
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            Counter.logIncrementWithUid(
-                    "virtual_devices.value_virtual_display_created_count",
-                    mAttributionSource.getUid());
-        }
+        Counter.logIncrementWithUid(
+                "virtual_devices.value_virtual_display_created_count",
+                mAttributionSource.getUid());
         return displayId;
     }
 
@@ -1376,11 +1413,9 @@
             showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_secure_window,
                     Toast.LENGTH_LONG, mContext.getMainLooper());
 
-            if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-                Counter.logIncrementWithUid(
-                        "virtual_devices.value_secure_window_blocked_count",
-                        mAttributionSource.getUid());
-            }
+            Counter.logIncrementWithUid(
+                    "virtual_devices.value_secure_window_blocked_count",
+                    mAttributionSource.getUid());
         }
     }
 
@@ -1441,20 +1476,24 @@
 
     @SuppressWarnings("AndroidFrameworkRequiresPermission")
     private void checkVirtualInputDeviceDisplayIdAssociation(int displayId) {
+        // The INJECT_EVENTS permission allows for injecting input to any window / display.
         if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.INJECT_EVENTS)
-                    == PackageManager.PERMISSION_GRANTED) {
-            // The INJECT_EVENTS permission allows for injecting input to any window / display.
-            return;
-        }
-        synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplays.contains(displayId)) {
-                throw new SecurityException(
-                        "Cannot create a virtual input device for display " + displayId
-                                + " which not associated with this virtual device");
+                    != PackageManager.PERMISSION_GRANTED) {
+            synchronized (mVirtualDeviceLock) {
+                checkDisplayOwnedByVirtualDeviceLocked(displayId);
             }
         }
     }
 
+    @GuardedBy("mVirtualDeviceLock")
+    private void checkDisplayOwnedByVirtualDeviceLocked(int displayId) {
+        if (!mVirtualDisplays.contains(displayId)) {
+            throw new SecurityException(
+                    "Invalid displayId: Display " + displayId
+                            + " is not associated with this virtual device");
+        }
+    }
+
     /**
      * Release resources tied to virtual display owned by this VirtualDevice instance.
      *
@@ -1579,8 +1618,8 @@
                 // Explicitly match the actions because the intent filter will match any intent
                 // without an explicit action. If the intent has no action, then require that there
                 // are no actions specified in the filter either.
-                boolean explicitActionMatch = !intentInterceptionActionMatchingFix()
-                        || intent.getAction() != null || intentFilter.countActions() == 0;
+                boolean explicitActionMatch =
+                        intent.getAction() != null || intentFilter.countActions() == 0;
                 if (explicitActionMatch && intentFilter.match(
                         intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(),
                         intent.getCategories(), TAG) >= 0) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 1be1d2b..3cd1ca4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -474,9 +474,7 @@
                     deviceId,
                     cameraAccessController, mPendingTrampolineCallback, activityListener,
                     soundEffectListener, runningAppsChangedCallback, params);
-            if (Flags.expressMetrics()) {
-                Counter.logIncrement("virtual_devices.value_virtual_devices_created_count");
-            }
+            Counter.logIncrement("virtual_devices.value_virtual_devices_created_count");
 
             synchronized (mVirtualDeviceManagerLock) {
                 if (!Flags.persistentDeviceIdApi() && mVirtualDevices.size() == 0) {
@@ -500,11 +498,9 @@
                     }
                 });
             }
-            if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-                Counter.logIncrementWithUid(
-                        "virtual_devices.value_virtual_devices_created_with_uid_count",
-                        attributionSource.getUid());
-            }
+            Counter.logIncrementWithUid(
+                    "virtual_devices.value_virtual_devices_created_with_uid_count",
+                    attributionSource.getUid());
             return virtualDevice;
         }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/audio/VirtualAudioController.java b/services/companion/java/com/android/server/companion/virtual/audio/VirtualAudioController.java
index 4bffb76..d3b3945 100644
--- a/services/companion/java/com/android/server/companion/virtual/audio/VirtualAudioController.java
+++ b/services/companion/java/com/android/server/companion/virtual/audio/VirtualAudioController.java
@@ -77,11 +77,9 @@
         mAudioPlaybackDetector = new AudioPlaybackDetector(context);
         mAudioRecordingDetector = new AudioRecordingDetector(context);
 
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            Counter.logIncrementWithUid(
-                    "virtual_devices.value_virtual_audio_created_count",
-                    attributionSource.getUid());
-        }
+        Counter.logIncrementWithUid(
+                "virtual_devices.value_virtual_audio_created_count",
+                attributionSource.getUid());
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 62efafb..7b81ef3 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -102,11 +102,9 @@
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
-        if (android.companion.virtualdevice.flags.Flags.metricsCollection()) {
-            Counter.logIncrementWithUid(
-                    "virtual_devices.value_virtual_camera_created_count",
-                    attributionSource.getUid());
-        }
+        Counter.logIncrementWithUid(
+                "virtual_devices.value_virtual_camera_created_count",
+                attributionSource.getUid());
     }
 
     /**
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 33cf842..fdf0ba6 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -3535,6 +3535,10 @@
 
         synchronized (mRecords) {
             int phoneId = getPhoneIdFromSubId(subId);
+            if (!validatePhoneId(phoneId)) {
+                loge("Invalid phone ID " + phoneId + " for " + subId);
+                return;
+            }
             mCarrierRoamingNtnMode[phoneId] = active;
             for (Record r : mRecords) {
                 if (r.matchTelephonyCallbackEvent(
@@ -3582,6 +3586,10 @@
 
         synchronized (mRecords) {
             int phoneId = getPhoneIdFromSubId(subId);
+            if (!validatePhoneId(phoneId)) {
+                loge("Invalid phone ID " + phoneId + " for " + subId);
+                return;
+            }
             mCarrierRoamingNtnEligible[phoneId] = eligible;
             for (Record r : mRecords) {
                 if (r.matchTelephonyCallbackEvent(
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index cf0befa..33f33fb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1716,6 +1716,12 @@
      */
     @Nullable volatile ContentCaptureManagerInternal mContentCaptureService;
 
+    /**
+     * The interface to the freezer.
+     */
+    @NonNull
+    private final Freezer mFreezer;
+
     /*
      * The default duration for the binder heavy hitter auto sampler
      */
@@ -2506,6 +2512,7 @@
             @Nullable UserController userController) {
         mInjector = injector;
         mContext = mInjector.getContext();
+        mFreezer = injector.getFreezer();
         mUiContext = null;
         mAppErrors = injector.getAppErrors();
         mPackageWatchdog = null;
@@ -2555,6 +2562,7 @@
         LockGuard.installLock(this, LockGuard.INDEX_ACTIVITY);
         mInjector = new Injector(systemContext);
         mContext = systemContext;
+        mFreezer = mInjector.getFreezer();
 
         mFactoryTest = FactoryTest.getMode();
         mSystemThread = ActivityThread.currentActivityThread();
@@ -20919,6 +20927,11 @@
         public IntentFirewall getIntentFirewall() {
             return null;
         }
+
+        /** @return the default Freezer. */
+        public Freezer getFreezer() {
+            return new Freezer();
+        }
     }
 
     @Override
@@ -21022,7 +21035,7 @@
         final long token = Binder.clearCallingIdentity();
 
         try {
-            return CachedAppOptimizer.isFreezerSupported();
+            return mFreezer.isFreezerSupported();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -21177,4 +21190,9 @@
     void clearPendingTopAppLocked() {
         mPendingStartActivityUids.clear();
     }
+
+    @NonNull
+    Freezer getFreezer() {
+        return mFreezer;
+    }
 }
diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java
index ee7033e..415f78a 100644
--- a/services/core/java/com/android/server/am/AppErrors.java
+++ b/services/core/java/com/android/server/am/AppErrors.java
@@ -65,7 +65,6 @@
 import com.android.server.LocalServices;
 import com.android.server.PackageWatchdog;
 import com.android.server.pm.UserManagerInternal;
-import com.android.server.pm.UserManagerService;
 import com.android.server.usage.AppStandbyInternal;
 import com.android.server.wm.WindowProcessController;
 
@@ -1027,7 +1026,8 @@
                 isBackground &= (userId != profileId);
             }
             int visibleUserId = getVisibleUserId(userId);
-            boolean isVisibleUser = isVisibleBackgroundUser(visibleUserId);
+            boolean isVisibleUser = LocalServices.getService(UserManagerInternal.class)
+                    .isVisibleBackgroundFullUser(visibleUserId);
             boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                     Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0;
             if (isBackground && !showBackground && !isVisibleUser) {
@@ -1050,7 +1050,7 @@
                         mContext.getContentResolver(),
                         Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
                         0,
-                        mService.mUserController.getCurrentUserId()) != 0;
+                        visibleUserId) != 0;
                 final String packageName = proc.info.packageName;
                 final boolean crashSilenced = mAppsNotReportingCrashes != null
                         && mAppsNotReportingCrashes.contains(proc.info.packageName);
@@ -1183,26 +1183,6 @@
     }
 
     /**
-     * Checks if the given user is a visible background user, which is a full, background user
-     * assigned to secondary displays on the devices that have
-     * {@link UserManager#isVisibleBackgroundUsersEnabled()
-     * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on
-     * automotive builds, using the display associated with their seats).
-     *
-     * @see UserManager#isUserVisible()
-     */
-    private boolean isVisibleBackgroundUser(int userId) {
-        if (!UserManager.isVisibleBackgroundUsersEnabled()) {
-            return false;
-        }
-        boolean isForeground = mService.mUserController.getCurrentUserId() == userId;
-        boolean isProfile = UserManagerService.getInstance().isProfile(userId);
-        boolean isVisible = LocalServices.getService(UserManagerInternal.class)
-                .isUserVisible(userId);
-        return isVisible && !isForeground && !isProfile;
-    }
-
-    /**
      * Information about a process that is currently marked as bad.
      */
     static final class BadProcessInfo {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 03fbfd37..6333159 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -620,6 +620,10 @@
 
     private void setPowerStatsThrottlePeriods(BatteryStatsImpl.BatteryStatsConfig.Builder builder,
             String configString) {
+        if (configString == null) {
+            return;
+        }
+
         Matcher matcher = Pattern.compile("([^:]+):(\\d+)\\s*").matcher(configString);
         while (matcher.find()) {
             String powerComponentName = matcher.group(1);
@@ -1256,7 +1260,14 @@
                                     .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
                                     .build();
                     bus = getBatteryUsageStats(List.of(query)).get(0);
-                    return new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
+                    final int pullResult =
+                            new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
+                    try {
+                        bus.close();
+                    } catch (IOException e) {
+                        Slog.w(TAG, "Failure close BatteryUsageStats", e);
+                    }
+                    return pullResult;
                 }
                 default:
                     throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 1c4ffbb..11e8353 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -664,6 +664,8 @@
     private final ProcessDependencies mProcessDependencies;
     private final ProcLocksReader mProcLocksReader;
 
+    private final Freezer mFreezer;
+
     public CachedAppOptimizer(ActivityManagerService am) {
         this(am, null, new DefaultProcessDependencies());
     }
@@ -680,6 +682,7 @@
         mTestCallback = callback;
         mSettingsObserver = new SettingsContentObserver();
         mProcLocksReader = new ProcLocksReader();
+        mFreezer = mAm.getFreezer();
     }
 
     /**
@@ -1050,89 +1053,6 @@
     }
 
     /**
-     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
-     * this method, this method will synchronously dispatch all pending transactions to the
-     * specified pid. This method will not add significant latencies when unfreezing.
-     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
-     * an error to the sending process.
-     *
-     * @param pid the target pid for which binder transactions are to be frozen
-     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
-     * binder for the specificed pid.
-     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
-     * before giving up.
-     *
-     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
-     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
-     */
-    public static native int freezeBinder(int pid, boolean freeze, int timeoutMs);
-
-    /**
-     * Retrieves binder freeze info about a process.
-     * @param pid the pid for which binder freeze info is to be retrieved.
-     *
-     * @throws RuntimeException if the operation could not complete successfully.
-     * @return a bit field reporting the binder freeze info for the process.
-     */
-    private static native int getBinderFreezeInfo(int pid);
-
-    /**
-     * Returns the path to be checked to verify whether the freezer is supported by this system.
-     * @return absolute path to the file
-     */
-    private static native String getFreezerCheckPath();
-
-    /**
-     * Check if task_profiles.json includes valid freezer profiles and actions
-     * @return false if there are invalid profiles or actions
-     */
-    private static native boolean isFreezerProfileValid();
-
-    /**
-     * Determines whether the freezer is supported by this system
-     */
-    public static boolean isFreezerSupported() {
-        boolean supported = false;
-        FileReader fr = null;
-
-        try {
-            String path = getFreezerCheckPath();
-            Slog.d(TAG_AM, "Checking cgroup freezer: " + path);
-            fr = new FileReader(path);
-            char state = (char) fr.read();
-
-            if (state == '1' || state == '0') {
-                // Also check freezer binder ioctl
-                Slog.d(TAG_AM, "Checking binder freezer ioctl");
-                getBinderFreezeInfo(Process.myPid());
-
-                // Check if task_profiles.json contains invalid profiles
-                Slog.d(TAG_AM, "Checking freezer profiles");
-                supported = isFreezerProfileValid();
-            } else {
-                Slog.e(TAG_AM, "Unexpected value in cgroup.freeze");
-            }
-        } catch (java.io.FileNotFoundException e) {
-            Slog.w(TAG_AM, "File cgroup.freeze not present");
-        } catch (RuntimeException e) {
-            Slog.w(TAG_AM, "Unable to read freezer info");
-        } catch (Exception e) {
-            Slog.w(TAG_AM, "Unable to read cgroup.freeze: " + e.toString());
-        }
-
-        if (fr != null) {
-            try {
-                fr.close();
-            } catch (java.io.IOException e) {
-                Slog.e(TAG_AM, "Exception closing cgroup.freeze: " + e.toString());
-            }
-        }
-
-        Slog.d(TAG_AM, "Freezer supported: " + supported);
-        return supported;
-    }
-
-    /**
      * Reads the flag value from DeviceConfig to determine whether app freezer
      * should be enabled, and starts the freeze/compaction thread if needed.
      */
@@ -1146,7 +1066,7 @@
         } else if ("enabled".equals(configOverride)
                 || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                     KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) {
-            mUseFreezer = isFreezerSupported();
+            mUseFreezer = mFreezer.isFreezerSupported();
             updateFreezerDebounceTimeout();
             updateFreezerExemptInstPkg();
         } else {
@@ -1528,7 +1448,7 @@
         boolean processKilled = false;
 
         try {
-            int freezeInfo = getBinderFreezeInfo(pid);
+            int freezeInfo = mFreezer.getBinderFreezeInfo(pid);
 
             if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
                 Slog.d(TAG_AM, "pid " + pid + " " + app.processName
@@ -1562,7 +1482,7 @@
         long freezeTime = opt.getFreezeUnfreezeTime();
 
         try {
-            freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
+            mFreezer.freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
         } catch (RuntimeException e) {
             Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
                     + ". Killing it");
@@ -1574,7 +1494,7 @@
 
         try {
             traceAppFreeze(app.processName, pid, reason);
-            Process.setProcessFrozen(pid, app.uid, false);
+            mFreezer.setProcessFrozen(pid, app.uid, false);
 
             opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
             opt.setFrozen(false);
@@ -1617,7 +1537,7 @@
             }
             Slog.d(TAG_AM, "quick sync unfreeze " + pid + " for " +  reason);
             try {
-                freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
+                mFreezer.freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
             } catch (RuntimeException e) {
                 Slog.e(TAG_AM, "Unable to quick unfreeze binder for " + pid);
                 return;
@@ -1625,7 +1545,7 @@
 
             try {
                 traceAppFreeze(app.processName, pid, reason);
-                Process.setProcessFrozen(pid, app.uid, false);
+                mFreezer.setProcessFrozen(pid, app.uid, false);
             } catch (Exception e) {
                 Slog.e(TAG_AM, "Unable to quick unfreeze " + pid);
             }
@@ -2394,7 +2314,7 @@
                 // Freeze binder interface before the process, to flush any
                 // transactions that might be pending.
                 try {
-                    if (freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
+                    if (mFreezer.freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
                         handleBinderFreezerFailure(proc, "outstanding txns");
                         return;
                     }
@@ -2413,7 +2333,7 @@
 
                 try {
                     traceAppFreeze(proc.processName, pid, -1);
-                    Process.setProcessFrozen(pid, proc.uid, true);
+                    mFreezer.setProcessFrozen(pid, proc.uid, true);
                     opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
                     opt.setFrozen(true);
                     opt.setHasCollectedFrozenPSS(false);
@@ -2452,7 +2372,7 @@
 
             try {
                 // post-check to prevent races
-                int freezeInfo = getBinderFreezeInfo(pid);
+                int freezeInfo = mFreezer.getBinderFreezeInfo(pid);
 
                 if ((freezeInfo & TXNS_PENDING_WHILE_FROZEN) != 0) {
                     synchronized (mProcLock) {
@@ -2620,6 +2540,22 @@
     }
 
     /**
+     * Freeze or unfreeze a process.  This should only be used for testing.
+     */
+    @VisibleForTesting
+    void forceFreezeForTest(ProcessRecord proc, boolean freeze) {
+        synchronized (mAm) {
+            synchronized (mProcLock) {
+                if (freeze) {
+                    forceFreezeAppAsyncLSP(proc);
+                } else {
+                    unfreezeAppInternalLSP(proc, UNFREEZE_REASON_NONE, true);
+                }
+            }
+        }
+    }
+
+    /**
      * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
      * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
      * async binder transactions due to kernel binder buffer running out.
@@ -2660,7 +2596,7 @@
         for (int i = 0; i < pids.size(); i++) {
             int current = pids.get(i);
             try {
-                int freezeInfo = getBinderFreezeInfo(current);
+                int freezeInfo = mFreezer.getBinderFreezeInfo(current);
 
                 if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
                     killProcess(current, "Sync transaction while frozen",
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index afb7bb4..1314521 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1806,10 +1806,12 @@
                         ActivityManagerService.WAIT_FOR_CONTENT_PROVIDER_TIMEOUT_MSG, cpr);
             }
             final int userId = UserHandle.getUserId(cpr.uid);
+            boolean removed = false;
             // Don't remove from provider map if it doesn't match
             // could be a new content provider is starting
             if (mProviderMap.getProviderByClass(cpr.name, userId) == cpr) {
                 mProviderMap.removeProviderByClass(cpr.name, userId);
+                removed = true;
             }
             String[] names = cpr.info.authority.split(";");
             for (int j = 0; j < names.length; j++) {
@@ -1817,8 +1819,12 @@
                 // could be a new content provider is starting
                 if (mProviderMap.getProviderByName(names[j], userId) == cpr) {
                     mProviderMap.removeProviderByName(names[j], userId);
+                    removed = true;
                 }
             }
+            if (removed && cpr.proc != null) {
+                cpr.proc.mProviders.removeProvider(cpr.info.name);
+            }
         }
 
         for (int i = cpr.connections.size() - 1; i >= 0; i--) {
diff --git a/services/core/java/com/android/server/am/Freezer.java b/services/core/java/com/android/server/am/Freezer.java
new file mode 100644
index 0000000..3b3cf55
--- /dev/null
+++ b/services/core/java/com/android/server/am/Freezer.java
@@ -0,0 +1,115 @@
+/*
+ * 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.server.am;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+
+import android.os.Process;
+
+/**
+ * A collection of interfaces to manage the freezer.  All access to the freezer goes through an
+ * instance of this class.  The class can be overridden for testing.
+ *
+ * Methods may be called without external synchronization.  Multiple instances of this class can be
+ * used concurrently.
+ */
+class Freezer {
+
+    /**
+     * Freeze or unfreeze the specified process.
+     *
+     * @param pid Identifier of the process to freeze or unfreeze.
+     * @param uid Identifier of the user the process is running under.
+     * @param frozen Specify whether to free (true) or unfreeze (false).
+     */
+    public void setProcessFrozen(int pid, int uid, boolean frozen) {
+        Process.setProcessFrozen(pid, uid, frozen);
+    }
+
+    /**
+     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
+     * this method, this method will synchronously dispatch all pending transactions to the
+     * specified pid. This method will not add significant latencies when unfreezing.
+     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
+     * an error to the sending process.
+     *
+     * @param pid the target pid for which binder transactions are to be frozen
+     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
+     * binder for the specified pid.
+     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+     * before giving up.
+     *
+     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
+     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
+     */
+    public int freezeBinder(int pid, boolean freeze, int timeoutMs) {
+        return nativeFreezeBinder(pid, freeze, timeoutMs);
+    }
+
+    /**
+     * Retrieves binder freeze info about a process.
+     * @param pid the pid for which binder freeze info is to be retrieved.
+     *
+     * @throws RuntimeException if the operation could not complete successfully.
+     * @return a bit field reporting the binder freeze info for the process.
+     */
+    public int getBinderFreezeInfo(int pid) {
+        return nativeGetBinderFreezeInfo(pid);
+    }
+
+    /**
+     * Determines whether the freezer is supported by this system.
+     * @return true if the freezer is supported.
+     */
+    public boolean isFreezerSupported() {
+        return nativeIsFreezerSupported();
+    }
+
+    // Native methods
+
+    /**
+     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
+     * this method, this method will synchronously dispatch all pending transactions to the
+     * specified pid. This method will not add significant latencies when unfreezing.
+     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
+     * an error to the sending process.
+     *
+     * @param pid the target pid for which binder transactions are to be frozen
+     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
+     * binder for the specified pid.
+     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+     * before giving up.
+     *
+     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
+     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
+     */
+    private static native int nativeFreezeBinder(int pid, boolean freeze, int timeoutMs);
+
+    /**
+     * Retrieves binder freeze info about a process.
+     * @param pid the pid for which binder freeze info is to be retrieved.
+     *
+     * @throws RuntimeException if the operation could not complete successfully.
+     * @return a bit field reporting the binder freeze info for the process.
+     */
+    private static native int nativeGetBinderFreezeInfo(int pid);
+
+    /**
+     * Return 0 if the freezer is supported on this platform and -1 otherwise.
+     */
+    private static native boolean nativeIsFreezerSupported();
+}
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index b517631..d6f04db 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -39,6 +39,7 @@
 per-file ContentProviderHelper.java = varunshah@google.com, omakoto@google.com, jsharkey@google.com, yamasani@google.com
 
 per-file CachedAppOptimizer.java = file:/PERFORMANCE_OWNERS
+per-file Freezer.java = file:/PERFORMANCE_OWNERS
 
 # Multiuser
 per-file User* = file:/MULTIUSER_OWNERS
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 8eef71e..6857b6b 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -19,9 +19,11 @@
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
 import static android.os.Process.ROOT_UID;
 import static android.os.Process.SYSTEM_UID;
 
@@ -367,17 +369,6 @@
     }
 
     /**
-     * Return true if the activity options allows PendingIntent to use caller's BAL permission.
-     */
-    public static boolean isPendingIntentBalAllowedByPermission(
-            @Nullable ActivityOptions activityOptions) {
-        if (activityOptions == null) {
-            return false;
-        }
-        return activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission();
-    }
-
-    /**
      * Return the {@link BackgroundStartPrivileges} the activity options grant the PendingIntent to
      * use caller's BAL permission.
      */
@@ -404,6 +395,8 @@
                 return BackgroundStartPrivileges.NONE;
             case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
                 return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS:
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE:
             case MODE_BACKGROUND_ACTIVITY_START_ALLOWED:
             case MODE_BACKGROUND_ACTIVITY_START_COMPAT:
             default:
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 726e827..bb0c24b 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3005,7 +3005,7 @@
         return freezePackageCgroup(packageUID, false);
     }
 
-    private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
+    private void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
                                                      int packageUID) {
         // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
         // Since we're going to kill these, we don't need to unfreze them later.
@@ -3019,7 +3019,7 @@
                 try {
                     int rc;
                     do {
-                        rc = CachedAppOptimizer.freezeBinder(pid, true, 10 /* timeout_ms */);
+                        rc = mService.getFreezer().freezeBinder(pid, true, 10 /* timeout_ms */);
                     } while (rc == -EAGAIN && nRetries++ < 1);
                     if (rc != 0) Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + rc);
                 } catch (RuntimeException e) {
diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig
index 15c8850..b2e95aa 100644
--- a/services/core/java/com/android/server/biometrics/biometrics.aconfig
+++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig
@@ -16,8 +16,11 @@
 }
 
 flag {
-  name: "notify_fingerprint_loe"
+  name: "notify_fingerprints_loe"
   namespace: "biometrics_framework"
   description: "This flag controls whether a notification should be sent to notify user when loss of enrollment happens"
   bug: "351036558"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
index 3b6aeef..77e27ba 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
@@ -108,7 +108,7 @@
             }
 
             if (mBiometricUtils.hasValidBiometricUserState(getContext(), getTargetUserId())
-                    && Flags.notifyFingerprintLoe()) {
+                    && Flags.notifyFingerprintsLoe()) {
                 handleInvalidBiometricState();
             }
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index ef7abdd..6cce722 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -227,6 +227,9 @@
                 onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);
                 mCallback.onClientFinished(this, false /* success */);
             }
+        } else {
+            Slog.e(TAG, "Cancellation signal is null");
+            mCallback.onClientFinished(this, false /* success */);
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index d04afdb..dee4b4f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -359,7 +359,8 @@
                 mCallback.onClientFinished(this, false /* success */);
             }
         } else {
-            Slog.e(TAG, "cancellation signal was null");
+            Slog.e(TAG, "Cancellation signal was null");
+            mCallback.onClientFinished(this, false /* success */);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 8bb33dd..53d6768 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -803,15 +803,27 @@
     @Override
     public void overrideDozeScreenState(int displayState, @Display.StateReason int reason) {
         Slog.i(TAG, "New offload doze override: " + Display.stateToString(displayState));
-        mHandler.postAtTime(() -> {
-            if (mDisplayOffloadSession == null
-                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
-                            || displayState == Display.STATE_UNKNOWN)) {
-                return;
+        if (mDisplayOffloadSession != null
+                && DisplayOffloadSession.isSupportedOffloadState(displayState)
+                && displayState != Display.STATE_UNKNOWN) {
+            if (mFlags.isOffloadDozeOverrideHoldsWakelockEnabled()) {
+                boolean acquired = mWakelockController.acquireWakelock(
+                        WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE);
+                if (!acquired) {
+                    Slog.i(TAG, "A request to override the doze screen state is already "
+                            + "under process");
+                    return;
+                }
             }
-            mDisplayStateController.overrideDozeScreenState(displayState, reason);
-            updatePowerState();
-        }, mClock.uptimeMillis());
+            mHandler.postAtTime(() -> {
+                mDisplayStateController.overrideDozeScreenState(displayState, reason);
+                updatePowerState();
+                if (mFlags.isOffloadDozeOverrideHoldsWakelockEnabled()) {
+                    mWakelockController.releaseWakelock(
+                            WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE);
+                }
+            }, mClock.uptimeMillis());
+        }
     }
 
     @Override
@@ -1338,30 +1350,6 @@
             initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN);
         }
 
-        if (mFlags.isOffloadDozeOverrideHoldsWakelockEnabled()) {
-            // Sometimes, a display-state change can come without an associated PowerRequest,
-            // as with DisplayOffload.  For those cases, we have to make sure to also mark the
-            // display as "not ready" so that we can inform power-manager when the state-change is
-            // complete.
-            if (mPowerState.getScreenState() != state) {
-                final boolean wasReady;
-                synchronized (mLock) {
-                    wasReady = mDisplayReadyLocked;
-                    mDisplayReadyLocked = false;
-                    mustNotify = true;
-                }
-
-                if (wasReady) {
-                    // If we went from ready to not-ready from the state-change (instead of a
-                    // PowerRequest) there's a good chance that nothing is keeping PowerManager
-                    // from suspending. Grab the unfinished business suspend blocker to keep the
-                    // device awake until the display-state change goes into effect.
-                    mWakelockController.acquireWakelock(
-                            WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-                }
-            }
-        }
-
         // Animate the screen state change unless already animating.
         // The transition may be deferred, so after this point we will use the
         // actual state instead of the desired one.
diff --git a/services/core/java/com/android/server/display/WakelockController.java b/services/core/java/com/android/server/display/WakelockController.java
index 7bc7971..5b0229c 100644
--- a/services/core/java/com/android/server/display/WakelockController.java
+++ b/services/core/java/com/android/server/display/WakelockController.java
@@ -20,6 +20,7 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.utils.DebugUtils;
 
@@ -37,7 +38,8 @@
     public static final int WAKE_LOCK_PROXIMITY_NEGATIVE = 2;
     public static final int WAKE_LOCK_PROXIMITY_DEBOUNCE = 3;
     public static final int WAKE_LOCK_STATE_CHANGED = 4;
-    public static final int WAKE_LOCK_UNFINISHED_BUSINESS = 5;
+    public static final int WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE = 5;
+    public static final int WAKE_LOCK_UNFINISHED_BUSINESS = 6;
 
     @VisibleForTesting
     static final int WAKE_LOCK_MAX = WAKE_LOCK_UNFINISHED_BUSINESS;
@@ -53,18 +55,23 @@
             WAKE_LOCK_PROXIMITY_NEGATIVE,
             WAKE_LOCK_PROXIMITY_DEBOUNCE,
             WAKE_LOCK_STATE_CHANGED,
+            WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE,
             WAKE_LOCK_UNFINISHED_BUSINESS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface WAKE_LOCK_TYPE {
     }
 
+    private final Object mLock = new Object();
+
     // Asynchronous callbacks into the power manager service.
     // Only invoked from the handler thread while no locks are held.
     private final DisplayManagerInternal.DisplayPowerCallbacks mDisplayPowerCallbacks;
 
     // Identifiers for suspend blocker acquisition requests
     private final String mSuspendBlockerIdUnfinishedBusiness;
+    @GuardedBy("mLock")
+    private final String mSuspendBlockerOverrideDozeScreenState;
     private final String mSuspendBlockerIdOnStateChanged;
     private final String mSuspendBlockerIdProxPositive;
     private final String mSuspendBlockerIdProxNegative;
@@ -73,6 +80,10 @@
     // True if we have unfinished business and are holding a suspend-blocker.
     private boolean mUnfinishedBusiness;
 
+    // True if we have are holding a suspend-blocker to override the doze screen state.
+    @GuardedBy("mLock")
+    private boolean mIsOverrideDozeScreenStateAcquired;
+
     // True if we have have debounced the proximity change impact and are holding a suspend-blocker.
     private boolean mHasProximityDebounced;
 
@@ -108,6 +119,7 @@
         mTag = TAG + "[" + mDisplayId + "]";
         mDisplayPowerCallbacks = callbacks;
         mSuspendBlockerIdUnfinishedBusiness = "[" + displayId + "]unfinished business";
+        mSuspendBlockerOverrideDozeScreenState =  "[" + displayId + "]override doze screen state";
         mSuspendBlockerIdOnStateChanged = "[" + displayId + "]on state changed";
         mSuspendBlockerIdProxPositive = "[" + displayId + "]prox positive";
         mSuspendBlockerIdProxNegative = "[" + displayId + "]prox negative";
@@ -154,6 +166,10 @@
                 return acquireProxDebounceSuspendBlocker();
             case WAKE_LOCK_STATE_CHANGED:
                 return acquireStateChangedSuspendBlocker();
+            case WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE:
+                synchronized (mLock) {
+                    return acquireOverrideDozeScreenStateSuspendBlockerLocked();
+                }
             case WAKE_LOCK_UNFINISHED_BUSINESS:
                 return acquireUnfinishedBusinessSuspendBlocker();
             default:
@@ -171,6 +187,10 @@
                 return releaseProxDebounceSuspendBlocker();
             case WAKE_LOCK_STATE_CHANGED:
                 return releaseStateChangedSuspendBlocker();
+            case WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE:
+                synchronized (mLock) {
+                    return releaseOverrideDozeScreenStateSuspendBlockerLocked();
+                }
             case WAKE_LOCK_UNFINISHED_BUSINESS:
                 return releaseUnfinishedBusinessSuspendBlocker();
             default:
@@ -220,6 +240,42 @@
     }
 
     /**
+     * Acquires the suspend blocker to override the doze screen state and notifies the
+     * PowerManagerService about the changes. Note that this utility is syncronized because a
+     * request to override the doze screen state can come from a non-power thread.
+     */
+    @GuardedBy("mLock")
+    private boolean acquireOverrideDozeScreenStateSuspendBlockerLocked() {
+        // Grab a wake lock if we have unfinished business.
+        if (!mIsOverrideDozeScreenStateAcquired) {
+            if (DEBUG) {
+                Slog.d(mTag, "Acquiring suspend blocker to override the doze screen state...");
+            }
+            mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerOverrideDozeScreenState);
+            mIsOverrideDozeScreenStateAcquired = true;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Releases the override doze screen state suspend blocker and notifies the PowerManagerService
+     * about the changes.
+     */
+    @GuardedBy("mLock")
+    private boolean releaseOverrideDozeScreenStateSuspendBlockerLocked() {
+        if (mIsOverrideDozeScreenStateAcquired) {
+            if (DEBUG) {
+                Slog.d(mTag, "Finished overriding doze screen state...");
+            }
+            mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerOverrideDozeScreenState);
+            mIsOverrideDozeScreenStateAcquired = false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Acquires the unfinished business wakelock and notifies the PowerManagerService about the
      * changes.
      */
@@ -366,6 +422,7 @@
         pw.println("  mOnStateChangePending=" + isOnStateChangedPending());
         pw.println("  mOnProximityPositiveMessages=" + isProximityPositiveAcquired());
         pw.println("  mOnProximityNegativeMessages=" + isProximityNegativeAcquired());
+        pw.println("  mIsOverrideDozeScreenStateAcquired=" + isOverrideDozeScreenStateAcquired());
     }
 
     @VisibleForTesting
@@ -394,6 +451,13 @@
     }
 
     @VisibleForTesting
+    String getSuspendBlockerOverrideDozeScreenState() {
+        synchronized (mLock) {
+            return mSuspendBlockerOverrideDozeScreenState;
+        }
+    }
+
+    @VisibleForTesting
     boolean hasUnfinishedBusiness() {
         return mUnfinishedBusiness;
     }
@@ -417,4 +481,11 @@
     boolean hasProximitySensorDebounced() {
         return mHasProximityDebounced;
     }
+
+    @VisibleForTesting
+    boolean isOverrideDozeScreenStateAcquired() {
+        synchronized (mLock) {
+            return mIsOverrideDozeScreenStateAcquired;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 12c3197..59fffe7 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -347,7 +347,7 @@
                         data.mDisplayDeviceConfig));
             }
             if (flags.useNewHdrBrightnessModifier()) {
-                modifiers.add(new HdrBrightnessModifier(handler, listener, data));
+                modifiers.add(new HdrBrightnessModifier(handler, context, listener, data));
             }
             return modifiers;
         }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
index ae1801c..4ab4336 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
@@ -21,10 +21,15 @@
 
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.ContentObserver;
 import android.hardware.display.DisplayManagerInternal;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.view.SurfaceControlHdrLayerInfoListener;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -44,6 +49,11 @@
     static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f;
     private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f;
 
+    private final Uri mLowPowerModeSetting = Settings.Global.getUriFor(
+            Settings.Global.LOW_POWER_MODE);
+
+    private final ContentObserver mContentObserver;
+
     private final SurfaceControlHdrLayerInfoListener mHdrListener =
             new SurfaceControlHdrLayerInfoListener() {
                 @Override
@@ -52,7 +62,8 @@
                     boolean hdrLayerPresent = numberOfHdrLayers > 0;
                     mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged(
                             hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE,
-                            hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO));
+                            hdrLayerPresent ? Math.max(maxDesiredHdrSdrRatio,
+                                    DEFAULT_MAX_HDR_SDR_RATIO) : DEFAULT_MAX_HDR_SDR_RATIO));
                 }
             };
 
@@ -62,6 +73,7 @@
     private final Runnable mDebouncer;
 
     private IBinder mRegisteredDisplayToken;
+    private boolean mContentObserverRegistered = false;
 
     private DisplayDeviceConfig mDisplayDeviceConfig;
     @Nullable
@@ -73,6 +85,8 @@
 
     private float mAmbientLux = INVALID_LUX;
 
+    private boolean mLowPowerMode = false;
+
     private Mode mMode = Mode.NO_HDR;
     // The maximum brightness allowed for current lux
     private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
@@ -81,17 +95,17 @@
     private float mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
     private float mPendingTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
 
-    HdrBrightnessModifier(Handler handler,
+    HdrBrightnessModifier(Handler handler, Context context,
             BrightnessClamperController.ClamperChangeListener clamperChangeListener,
             BrightnessClamperController.DisplayDeviceData displayData) {
-        this(new Handler(handler.getLooper()), clamperChangeListener, new Injector(), displayData);
+        this(new Handler(handler.getLooper()), clamperChangeListener,
+                new Injector(context), displayData);
     }
 
     @VisibleForTesting
     HdrBrightnessModifier(Handler handler,
             BrightnessClamperController.ClamperChangeListener clamperChangeListener,
-            Injector injector,
-            BrightnessClamperController.DisplayDeviceData displayData) {
+            Injector injector, BrightnessClamperController.DisplayDeviceData displayData) {
         mHandler = handler;
         mClamperChangeListener = clamperChangeListener;
         mInjector = injector;
@@ -100,6 +114,12 @@
             mMaxBrightness = mPendingMaxBrightness;
             mClamperChangeListener.onChanged();
         };
+        mContentObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                onLowPowerModeChange();
+            }
+        };
         mHandler.post(() -> onDisplayChanged(displayData));
     }
 
@@ -135,12 +155,14 @@
         pw.println("  mMaxDesiredHdrRatio=" + mMaxDesiredHdrRatio);
         pw.println("  mHdrLayerSize=" + mHdrLayerSize);
         pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mLowPowerMode=" + mLowPowerMode);
         pw.println("  mMode=" + mMode);
         pw.println("  mMaxBrightness=" + mMaxBrightness);
         pw.println("  mPendingMaxBrightness=" + mPendingMaxBrightness);
         pw.println("  mTransitionRate=" + mTransitionRate);
         pw.println("  mPendingTransitionRate=" + mPendingTransitionRate);
         pw.println("  mHdrListener registered=" + (mRegisteredDisplayToken != null));
+        pw.println("  mContentObserverRegistered=" + mContentObserverRegistered);
     }
 
     // Called in DisplayControllerHandler
@@ -182,7 +204,25 @@
         } else {
             registerHdrListener(displayData.mDisplayToken);
         }
-        recalculate(data, mMaxDesiredHdrRatio);
+        if (data == null || data.allowInLowPowerMode) {
+            unregisterContentObserver();
+        } else {
+            registerContentObserver();
+        }
+
+        Mode newMode = recalculateMode(data);
+        // mode changed, or mode was HDR  and HdrBrightnessData changed
+        boolean needToNotifyChange = mMode != newMode
+                || (mMode != HdrBrightnessModifier.Mode.NO_HDR && data != mHdrBrightnessData);
+        mMode = newMode;
+        mHdrBrightnessData = data;
+        mMaxBrightness = findBrightnessLimit(mHdrBrightnessData, mAmbientLux);
+
+        if (needToNotifyChange) {
+            // data changed, reset custom transition rate
+            mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+            mClamperChangeListener.onChanged();
+        }
     }
 
     // Called in DisplayControllerHandler, when any modifier state changes
@@ -226,30 +266,6 @@
     }
 
     // Called in DisplayControllerHandler
-    private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) {
-        Mode newMode = recalculateMode(data);
-        // if HDR mode changed, notify changed
-        boolean needToNotifyChange = mMode != newMode;
-        // If HDR mode is active, we need to check if other HDR params are changed
-        if (mMode != HdrBrightnessModifier.Mode.NO_HDR) {
-            if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio)
-                    || data != mHdrBrightnessData) {
-                needToNotifyChange = true;
-            }
-        }
-
-        mMode = newMode;
-        mHdrBrightnessData = data;
-        mMaxDesiredHdrRatio = maxDesiredHdrRatio;
-
-        if (needToNotifyChange) {
-            // data or hdr layer changed, reset custom transition rate
-            mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
-            mClamperChangeListener.onChanged();
-        }
-    }
-
-    // Called in DisplayControllerHandler
     private Mode recalculateMode(@Nullable HdrBrightnessData data) {
         // no config
         if (data == null) {
@@ -259,6 +275,10 @@
         if (mHdrLayerSize == DEFAULT_HDR_LAYER_SIZE) {
             return Mode.NO_HDR;
         }
+        // low power mode and not allowed in low power mode
+        if (!data.allowInLowPowerMode && mLowPowerMode) {
+            return Mode.NO_HDR;
+        }
         // HDR layer < minHdr % for Nbm
         if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) {
             return Mode.NO_HDR;
@@ -271,6 +291,16 @@
         return Mode.HBM_HDR;
     }
 
+    private void onLowPowerModeChange() {
+        mLowPowerMode = mInjector.isLowPowerMode();
+        Mode newMode = recalculateMode(mHdrBrightnessData);
+        if (newMode != mMode) {
+            mMode = newMode;
+            mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+            mClamperChangeListener.onChanged();
+        }
+    }
+
     private float getMaxBrightness(Mode mode, float maxBrightness, HdrBrightnessData data) {
         if (mode == Mode.NBM_HDR) {
             return Math.min(data.hbmTransitionPoint, maxBrightness);
@@ -282,7 +312,13 @@
     }
 
     // Called in DisplayControllerHandler
-    private float findBrightnessLimit(HdrBrightnessData data, float ambientLux) {
+    private float findBrightnessLimit(@Nullable HdrBrightnessData data, float ambientLux) {
+        if (data == null) {
+            return PowerManager.BRIGHTNESS_MAX;
+        }
+        if (ambientLux == INVALID_LUX) {
+            return PowerManager.BRIGHTNESS_MAX;
+        }
         float foundAmbientBoundary = Float.MAX_VALUE;
         float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX;
         for (Map.Entry<Float, Float> brightnessPoint :
@@ -300,7 +336,17 @@
     // Called in DisplayControllerHandler
     private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) {
         mHdrLayerSize = hdrLayerSize;
-        recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio);
+        Mode newMode = recalculateMode(mHdrBrightnessData);
+        // mode changed, or mode was HDR  and maxDesiredHdrRatio changed
+        boolean needToNotifyChange = mMode != newMode
+                || (mMode != HdrBrightnessModifier.Mode.NO_HDR
+                && !BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrSdrRatio));
+        mMode = newMode;
+        mMaxDesiredHdrRatio = maxDesiredHdrSdrRatio;
+        if (needToNotifyChange) {
+            mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+            mClamperChangeListener.onChanged();
+        }
     }
 
     // Called in DisplayControllerHandler
@@ -324,12 +370,36 @@
         }
     }
 
+    // Called in DisplayControllerHandler
+    private void registerContentObserver() {
+        if (!mContentObserverRegistered) {
+            mInjector.registerContentObserver(mContentObserver, mLowPowerModeSetting);
+            mContentObserverRegistered = true;
+            mLowPowerMode = mInjector.isLowPowerMode();
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private void unregisterContentObserver() {
+        if (mContentObserverRegistered) {
+            mInjector.unregisterContentObserver(mContentObserver);
+            mContentObserverRegistered = false;
+            mLowPowerMode = false;
+        }
+    }
+
     private enum Mode {
         NO_HDR, NBM_HDR, HBM_HDR
     }
 
     @SuppressLint("MissingPermission")
     static class Injector {
+        private final Context mContext;
+
+        Injector(Context context) {
+            mContext = context;
+        }
+
         void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
             listener.register(token);
         }
@@ -337,5 +407,19 @@
         void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
             listener.unregister(token);
         }
+
+        void registerContentObserver(ContentObserver observer, Uri uri) {
+            mContext.getContentResolver().registerContentObserver(uri, false,
+                    observer, UserHandle.USER_ALL);
+        }
+
+        void unregisterContentObserver(ContentObserver observer) {
+            mContext.getContentResolver().unregisterContentObserver(observer);
+        }
+
+        boolean isLowPowerMode() {
+            return Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0) != 0;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index e242164..e0aa9bf 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -21,11 +21,13 @@
 import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission;
 
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.GrammaticalInflectionManager;
 import android.app.IGrammaticalInflectionManager;
 import android.content.AttributionSource;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.os.Binder;
@@ -36,6 +38,7 @@
 import android.os.ShellCallback;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.os.UserManager;
 import android.permission.PermissionManager;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -271,6 +274,31 @@
                 throw new IllegalArgumentException("Unknown grammatical gender");
             }
 
+            // TODO(b/356895553): Don't allow profiles and background user to change system
+            //  grammaticalinflection
+            if (UserManager.isVisibleBackgroundUsersEnabled()
+                    && mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_AUTOMOTIVE)) {
+                // The check is added only for automotive devices. On automotive devices, it is
+                // possible that multiple users are visible simultaneously using visible background
+                // users. In such cases, it is desired that only the current user (not the visible
+                // background user) can change the GrammaticalInflection of the device.
+                final long origId = Binder.clearCallingIdentity();
+                try {
+                    int currentUser = ActivityManager.getCurrentUser();
+                    if (userId != currentUser) {
+                        Log.w(TAG,
+                                "Only current user is allowed to update GrammaticalInflection if "
+                                        + "visible background users are enabled. Current User"
+                                        + currentUser + ". Calling User: " + userId);
+                        throw new SecurityException("Only current user is allowed to update "
+                                + "GrammaticalInflection.");
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(origId);
+                }
+            }
+
             final File file = getGrammaticalGenderFile(userId);
             synchronized (mLock) {
                 final AtomicFile atomicFile = new AtomicFile(file);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 730077c..76380b7 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -619,7 +619,7 @@
             case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
                 if (!mNewInputMethodSwitcherMenuEnabled) {
                     if (userId == mCurrentUserId) {
-                        mMenuController.updateKeyboardFromSettingsLocked();
+                        mMenuController.updateKeyboardFromSettingsLocked(userId);
                     }
                 }
                 break;
@@ -693,7 +693,7 @@
                                 senderUserId);
                     }
                 } else {
-                    mMenuController.hideInputMethodMenu();
+                    mMenuController.hideInputMethodMenu(senderUserId);
                 }
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
@@ -717,13 +717,19 @@
                 return;
             }
             for (int userId : mUserManagerInternal.getUserIds()) {
-                final InputMethodSettings settings = queryInputMethodServicesInternal(
-                        mContext,
-                        userId,
-                        AdditionalSubtypeMapRepository.get(userId),
-                        DirectBootAwareness.AUTO);
-                InputMethodSettingsRepository.put(userId, settings);
-
+                // Does InputMethodInfo really have data dependency on system locale?
+                // TODO(b/356679261): Check if we really need to update RawInputMethodInfo here.
+                {
+                    final var userData = getUserData(userId);
+                    final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId);
+                    final var rawMethodMap = queryRawInputMethodServiceMap(mContext, userId);
+                    userData.mRawInputMethodMap.set(rawMethodMap);
+                    final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap,
+                            DirectBootAwareness.AUTO,
+                            mUserManagerInternal.isUserUnlockingOrUnlocked(userId));
+                    final var settings = InputMethodSettings.create(methodMap, userId);
+                    InputMethodSettingsRepository.put(userId, settings);
+                }
                 postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */, userId);
                 // If the locale is changed, needs to reset the default ime
                 resetDefaultImeLocked(mContext, userId);
@@ -797,17 +803,13 @@
 
         private void onFinishPackageChangesInternal() {
             final int userId = getChangingUserId();
+            final var userData = getUserData(userId);
 
             // Instantiating InputMethodInfo requires disk I/O.
             // Do them before acquiring the lock to minimize the chances of ANR (b/340221861).
-            final var newMethodMapWithoutAdditionalSubtypes =
-                    queryInputMethodServicesInternal(mContext, userId,
-                            AdditionalSubtypeMap.EMPTY_MAP, DirectBootAwareness.AUTO)
-                            .getMethodMap();
+            userData.mRawInputMethodMap.set(queryRawInputMethodServiceMap(mContext, userId));
 
             synchronized (ImfLock.class) {
-                final AdditionalSubtypeMap additionalSubtypeMap =
-                        AdditionalSubtypeMapRepository.get(userId);
                 final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
 
                 InputMethodInfo curIm = null;
@@ -837,6 +839,7 @@
                 }
 
                 // Clear additional subtypes as a batch operation.
+                final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId);
                 final AdditionalSubtypeMap newAdditionalSubtypeMap =
                         additionalSubtypeMap.cloneWithRemoveOrSelf(imesToClearAdditionalSubtypes);
                 final boolean additionalSubtypeChanged =
@@ -846,8 +849,10 @@
                             settings.getMethodMap());
                 }
 
-                final var newMethodMap = newMethodMapWithoutAdditionalSubtypes
-                        .applyAdditionalSubtypes(newAdditionalSubtypeMap);
+                final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap(
+                        newAdditionalSubtypeMap,
+                        DirectBootAwareness.AUTO,
+                        mUserManagerInternal.isUserUnlockingOrUnlocked(userId));
 
                 if (InputMethodMap.areSame(settings.getMethodMap(), newMethodMap)) {
                     // No update in the actual IME map.
@@ -1065,9 +1070,11 @@
             final int userId = user.getUserIdentifier();
             SecureSettingsWrapper.onUserUnlocking(userId);
             mService.mIoHandler.post(() -> {
-                final var settings = queryInputMethodServicesInternal(mService.mContext, userId,
-                        AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
-                InputMethodSettingsRepository.put(userId, settings);
+                final var userData = mService.getUserData(userId);
+                final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap(
+                        AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO, true);
+                final var newSettings = InputMethodSettings.create(methodMap, userId);
+                InputMethodSettingsRepository.put(userId, newSettings);
                 synchronized (ImfLock.class) {
                     if (!mService.mSystemReady) {
                         return;
@@ -1105,19 +1112,22 @@
 
                 for (int userId : userIds) {
                     Slog.d(TAG, "Start initialization for user=" + userId);
+                    final var userData = mService.getUserData(userId);
+
                     AdditionalSubtypeMapRepository.initializeIfNecessary(userId);
                     final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId);
-                    final var settings = InputMethodManagerService.queryInputMethodServicesInternal(
-                            context, userId, additionalSubtypeMap,
-                            DirectBootAwareness.AUTO).getMethodMap();
-                    InputMethodSettingsRepository.put(userId,
-                            InputMethodSettings.create(settings, userId));
+                    final var rawMethodMap = queryRawInputMethodServiceMap(context, userId);
+                    userData.mRawInputMethodMap.set(rawMethodMap);
+                    final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap,
+                            DirectBootAwareness.AUTO,
+                            userManagerInternal.isUserUnlockingOrUnlocked(userId));
+                    final var settings = InputMethodSettings.create(methodMap, userId);
+                    InputMethodSettingsRepository.put(userId, settings);
 
                     final int profileParentId = userManagerInternal.getProfileParentId(userId);
                     final boolean value =
                             InputMethodDrawsNavBarResourceMonitor.evaluate(context,
                                     profileParentId);
-                    final var userData = mService.getUserData(userId);
                     userData.mImeDrawsNavBar.set(value);
 
                     userData.mBackgroundLoadLatch.countDown();
@@ -1132,12 +1142,13 @@
             // Called on ActivityManager thread.
             SecureSettingsWrapper.onUserStopped(userId);
             mService.mIoHandler.post(() -> {
+                final var userData = mService.getUserData(userId);
                 final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId);
-                final var settings = InputMethodManagerService.queryInputMethodServicesInternal(
-                        mService.mContext, userId, additionalSubtypeMap,
-                        DirectBootAwareness.AUTO).getMethodMap();
+                final var rawMethodMap = userData.mRawInputMethodMap.get();
+                final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap,
+                        DirectBootAwareness.AUTO, false /* userUnlocked */);
                 InputMethodSettingsRepository.put(userId,
-                        InputMethodSettings.create(settings, userId));
+                        InputMethodSettings.create(methodMap, userId));
             });
         }
     }
@@ -1222,12 +1233,6 @@
         }
     }
 
-    @GuardedBy("ImfLock.class")
-    @UserIdInt
-    int getCurrentImeUserIdLocked() {
-        return mCurrentUserId;
-    }
-
     private final class InkWindowInitializer implements Runnable {
         public void run() {
             synchronized (ImfLock.class) {
@@ -1239,12 +1244,12 @@
         }
     }
 
-    private void onUpdateEditorToolType(int toolType) {
-        synchronized (ImfLock.class) {
-            IInputMethodInvoker curMethod = getCurMethodLocked();
-            if (curMethod != null) {
-                curMethod.updateEditorToolType(toolType);
-            }
+    @GuardedBy("ImfLock.class")
+    private void onUpdateEditorToolTypeLocked(@MotionEvent.ToolType int toolType,
+            @UserIdInt int userId) {
+        final var curMethod = getInputMethodBindingController(userId).getCurMethod();
+        if (curMethod != null) {
+            curMethod.updateEditorToolType(toolType);
         }
     }
 
@@ -1641,15 +1646,11 @@
 
     private List<InputMethodInfo> getInputMethodListInternal(@UserIdInt int userId,
             @DirectBootAwareness int directBootAwareness, int callingUid) {
-        final InputMethodSettings settings;
-        if (directBootAwareness == DirectBootAwareness.AUTO) {
-            settings = InputMethodSettingsRepository.get(userId);
-        } else {
-            final AdditionalSubtypeMap additionalSubtypeMap =
-                    AdditionalSubtypeMapRepository.get(userId);
-            settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
-                    directBootAwareness);
-        }
+        final var userData = getUserData(userId);
+        final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap(
+                AdditionalSubtypeMapRepository.get(userId), directBootAwareness,
+                mUserManagerInternal.isUserUnlockingOrUnlocked(userId));
+        final var settings = InputMethodSettings.create(methodMap, userId);
         // Create a copy.
         final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList());
         // filter caller's access to input methods
@@ -1826,7 +1827,7 @@
             if (mNewInputMethodSwitcherMenuEnabled) {
                 mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
             } else {
-                mMenuController.hideInputMethodMenuLocked();
+                mMenuController.hideInputMethodMenuLocked(userId);
             }
         }
     }
@@ -2860,7 +2861,7 @@
     void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
         updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
         if (!mNewInputMethodSwitcherMenuEnabled) {
-            mMenuController.updateKeyboardFromSettingsLocked();
+            mMenuController.updateKeyboardFromSettingsLocked(userId);
         }
     }
 
@@ -3463,9 +3464,9 @@
             userData.mCurStatsToken = null;
 
             if (Flags.useHandwritingListenerForTooltype()) {
-                maybeReportToolType();
+                maybeReportToolType(userId);
             } else if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
-                onUpdateEditorToolType(lastClickToolType);
+                onUpdateEditorToolTypeLocked(lastClickToolType, userId);
             }
             mVisibilityApplier.performShowIme(windowToken, statsToken,
                     mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
@@ -3480,7 +3481,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void maybeReportToolType() {
+    private void maybeReportToolType(@UserIdInt int userId) {
+        // TODO(b/356638981): This needs to be compatible with visible background users.
         int lastDeviceId = mInputManagerInternal.getLastUsedInputDeviceId();
         final InputManager im = mContext.getSystemService(InputManager.class);
         if (im == null) {
@@ -3499,7 +3501,7 @@
             // other toolTypes are irrelevant and reported as unknown.
             toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
         }
-        onUpdateEditorToolType(toolType);
+        onUpdateEditorToolTypeLocked(toolType, userId);
     }
 
     @Override
@@ -4347,6 +4349,7 @@
                         + subtype.getLocale() + ", " + subtype.getMode());
             }
         }
+        final var userData = getUserData(userId);
         synchronized (ImfLock.class) {
             if (!mSystemReady) {
                 return;
@@ -4362,9 +4365,10 @@
                         settings.getMethodMap());
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    final InputMethodSettings newSettings = queryInputMethodServicesInternal(
-                            mContext, userId, AdditionalSubtypeMapRepository.get(userId),
-                            DirectBootAwareness.AUTO);
+                    final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap(
+                            AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO,
+                            mUserManagerInternal.isUserUnlockingOrUnlocked(userId));
+                    final var newSettings = InputMethodSettings.create(methodMap, userId);
                     InputMethodSettingsRepository.put(userId, newSettings);
                     if (isCurrentUser) {
                         postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */,
@@ -5071,7 +5075,7 @@
             mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
         } else {
             mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
-                    lastInputMethodId, lastInputMethodSubtypeId, imList);
+                    lastInputMethodId, lastInputMethodSubtypeId, imList, userId);
         }
     }
 
@@ -5303,31 +5307,15 @@
     }
 
     @NonNull
-    static InputMethodSettings queryInputMethodServicesInternal(Context context,
-            @UserIdInt int userId, @NonNull AdditionalSubtypeMap additionalSubtypeMap,
-            @DirectBootAwareness int directBootAwareness) {
+    static RawInputMethodMap queryRawInputMethodServiceMap(Context context, @UserIdInt int userId) {
         final Context userAwareContext = context.getUserId() == userId
                 ? context
                 : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
 
-        final int directBootAwarenessFlags;
-        switch (directBootAwareness) {
-            case DirectBootAwareness.ANY:
-                directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AWARE
-                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
-                break;
-            case DirectBootAwareness.AUTO:
-                directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AUTO;
-                break;
-            default:
-                directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AUTO;
-                Slog.e(TAG, "Unknown directBootAwareness=" + directBootAwareness
-                        + ". Falling back to DirectBootAwareness.AUTO");
-                break;
-        }
         final int flags = PackageManager.GET_META_DATA
                 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
-                | directBootAwarenessFlags;
+                | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 
         // Beware that package visibility filtering will be enforced based on the effective calling
         // identity (Binder.getCallingUid()), but our use case always expect Binder.getCallingUid()
@@ -5343,14 +5331,11 @@
         final List<String> enabledInputMethodList =
                 InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId);
 
-        final InputMethodMap methodMap = filterInputMethodServices(
-                additionalSubtypeMap, enabledInputMethodList, userAwareContext, services);
-        return InputMethodSettings.create(methodMap, userId);
+        return filterInputMethodServices(enabledInputMethodList, userAwareContext, services);
     }
 
     @NonNull
-    static InputMethodMap filterInputMethodServices(
-            @NonNull AdditionalSubtypeMap additionalSubtypeMap,
+    static RawInputMethodMap filterInputMethodServices(
             List<String> enabledInputMethodList, Context userAwareContext,
             List<ResolveInfo> services) {
         final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
@@ -5371,7 +5356,7 @@
 
             try {
                 final InputMethodInfo imi = new InputMethodInfo(userAwareContext, ri,
-                        additionalSubtypeMap.get(imeId));
+                        Collections.emptyList());
                 if (imi.isVrOnly()) {
                     continue;  // Skip VR-only IME, which isn't supported for now.
                 }
@@ -5394,7 +5379,7 @@
                 Slog.wtf(TAG, "Unable to load input method " + imeId, e);
             }
         }
-        return InputMethodMap.of(methodMap);
+        return RawInputMethodMap.of(methodMap);
     }
 
     @GuardedBy("ImfLock.class")
@@ -5948,7 +5933,7 @@
                         final var bindingController = getInputMethodBindingController(userId);
                         mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
                     } else {
-                        mMenuController.hideInputMethodMenuLocked();
+                        mMenuController.hideInputMethodMenuLocked(userId);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index ba5c13e..f16a5a0 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -21,6 +21,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -77,13 +78,12 @@
     @GuardedBy("ImfLock.class")
     void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId,
             String preferredInputMethodId, int preferredInputMethodSubtypeId,
-            @NonNull List<ImeSubtypeListItem> imList) {
+            @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId) {
         if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
 
-        final int userId = mService.getCurrentImeUserIdLocked();
         final var bindingController = mService.getInputMethodBindingController(userId);
 
-        hideInputMethodMenuLocked();
+        hideInputMethodMenuLocked(userId);
 
         if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
             final InputMethodSubtype currentSubtype =
@@ -131,7 +131,7 @@
         }
         final Context dialogWindowContext = mDialogWindowContext.get(displayId);
         mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
-        mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
+        mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu(userId));
 
         final Context dialogContext = mDialogBuilder.getContext();
         final TypedArray a = dialogContext.obtainStyledAttributes(null,
@@ -162,7 +162,7 @@
                     isChecked, userId);
             // Ensure that the input method dialog is dismissed when changing
             // the hardware keyboard state.
-            hideInputMethodMenu();
+            hideInputMethodMenu(userId);
         });
 
         // Fill the list items with onClick listener, which takes care of IME (and subtype)
@@ -185,7 +185,7 @@
                     }
                     mService.setInputMethodLocked(im.getId(), subtypeId, userId);
                 }
-                hideInputMethodMenuLocked();
+                hideInputMethodMenuLocked(userId);
             }
         };
         mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
@@ -209,10 +209,10 @@
         mSwitchingDialog.show();
     }
 
-    void updateKeyboardFromSettingsLocked() {
+    void updateKeyboardFromSettingsLocked(@UserIdInt int userId) {
         mShowImeWithHardKeyboard =
                 SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
-                        false, mService.getCurrentImeUserIdLocked());
+                        false, userId);
         if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
                 && mSwitchingDialog.isShowing()) {
             final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
@@ -223,18 +223,22 @@
 
     /**
      * Hides the input method switcher menu.
+     *
+     * @param userId user ID for this operation
      */
-    void hideInputMethodMenu() {
+    void hideInputMethodMenu(@UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            hideInputMethodMenuLocked();
+            hideInputMethodMenuLocked(userId);
         }
     }
 
     /**
      * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}.
+     *
+     * @param userId user ID for this operation
      */
     @GuardedBy("ImfLock.class")
-    void hideInputMethodMenuLocked() {
+    void hideInputMethodMenuLocked(@UserIdInt int userId) {
         if (DEBUG) Slog.v(TAG, "Hide switching menu");
 
         if (mSwitchingDialog != null) {
@@ -242,8 +246,6 @@
             mSwitchingDialog = null;
             mSwitchingDialogTitleView = null;
 
-            // TODO(b/305849394): Make InputMethodMenuController multi-user aware
-            final int userId = mService.getCurrentImeUserIdLocked();
             mService.updateSystemUiLocked(userId);
             mService.sendOnNavButtonFlagsChangedToAllImesLocked();
             mDialogBuilder = null;
diff --git a/services/core/java/com/android/server/inputmethod/RawInputMethodMap.java b/services/core/java/com/android/server/inputmethod/RawInputMethodMap.java
new file mode 100644
index 0000000..4e39a3f
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/RawInputMethodMap.java
@@ -0,0 +1,105 @@
+/*
+ * 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.server.inputmethod;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.inputmethod.DirectBootAwareness;
+
+import java.util.List;
+
+/**
+ * This is quite similar to {@link InputMethodMap} with two major differences.
+ *
+ * <ul>
+ *     <li>Additional {@link android.view.inputmethod.InputMethodSubtype} is not included.</li>
+ *     <li>Always include direct-boot unaware {@link android.inputmethodservice.InputMethodService}.
+ *     </li>
+ * </ul>
+ *
+ * <p>As seen in {@link #toInputMethodMap(AdditionalSubtypeMap, int, boolean)}, you can consider
+ * this is a prototype data where you can always derive {@link InputMethodMap} with
+ * {@link AdditionalSubtypeMap} and a boolean information whether
+ * {@link com.android.server.pm.UserManagerInternal#isUserUnlockingOrUnlocked(int)} returns
+ * {@code true} or not.</p>
+ */
+final class RawInputMethodMap {
+    static final String TAG = "RawInputMethodMap";
+
+    private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP = new ArrayMap<>();
+
+    private final ArrayMap<String, InputMethodInfo> mMap;
+
+    static RawInputMethodMap emptyMap() {
+        return new RawInputMethodMap(EMPTY_MAP);
+    }
+
+    static RawInputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) {
+        return new RawInputMethodMap(map);
+    }
+
+    private RawInputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) {
+        mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map);
+    }
+
+    @AnyThread
+    @NonNull
+    List<InputMethodInfo> values() {
+        return List.copyOf(mMap.values());
+    }
+
+    @NonNull
+    InputMethodMap toInputMethodMap(@NonNull AdditionalSubtypeMap additionalSubtypeMap,
+            @DirectBootAwareness int directBootAwareness, boolean userUnlocked) {
+        final int size = mMap.size();
+        final var newMap = new ArrayMap<String, InputMethodInfo>(size);
+
+        final boolean requireDirectBootAwareFlag;
+        switch (directBootAwareness) {
+            case DirectBootAwareness.ANY -> requireDirectBootAwareFlag = false;
+            case DirectBootAwareness.AUTO -> requireDirectBootAwareFlag = !userUnlocked;
+            default -> {
+                requireDirectBootAwareFlag = !userUnlocked;
+                Slog.e(TAG, "Unknown directBootAwareness=" + directBootAwareness
+                        + ". Falling back to DirectBootAwareness.AUTO");
+            }
+        }
+
+        boolean updated = false;
+        for (int i = 0; i < size; ++i) {
+            final var imeId = mMap.keyAt(i);
+            final var imi = mMap.valueAt(i);
+            if (requireDirectBootAwareFlag && !imi.getServiceInfo().directBootAware) {
+                updated = true;
+                continue;
+            }
+            final var newAdditionalSubtypes = additionalSubtypeMap.get(imeId);
+            if (newAdditionalSubtypes == null || newAdditionalSubtypes.isEmpty()) {
+                newMap.put(imi.getId(), imi);
+            } else {
+                updated = true;
+                newMap.put(imi.getId(), new InputMethodInfo(imi, newAdditionalSubtypes));
+            }
+        }
+        // If newMap is semantically the same as mMap, we can reuse mMap and discard newMap.
+        return InputMethodMap.of(updated ? newMap : mMap);
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java
index be57321..4fb55e1 100644
--- a/services/core/java/com/android/server/inputmethod/UserData.java
+++ b/services/core/java/com/android/server/inputmethod/UserData.java
@@ -30,6 +30,7 @@
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 /** Placeholder for all IMMS user specific fields */
 final class UserData {
@@ -43,6 +44,17 @@
     @NonNull
     final CountDownLatch mBackgroundLoadLatch = new CountDownLatch(1);
 
+    /**
+     * Contains non-null {@link RawInputMethodMap}, which represents the latest collections of
+     * {@link android.view.inputmethod.InputMethodInfo} for both direct-boot aware and unaware IMEs
+     * before taking {@link AdditionalSubtypeMap} into account.
+     *
+     * <p>See {@link RawInputMethodMap} for details on when to use this.</p>
+     */
+    @NonNull
+    final AtomicReference<RawInputMethodMap> mRawInputMethodMap =
+            new AtomicReference<>(RawInputMethodMap.emptyMap());
+
     @NonNull
     final InputMethodBindingController mBindingController;
 
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index cd69eba..2a0b1af 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -717,9 +717,13 @@
         }
         mReliableMessageHostEndpointIdActiveSet.remove(transaction.getHostEndpointId());
 
-        Log.d(TAG, "Successfully completed reliable message transaction with "
-                + "message sequence number: " + transaction.getMessageSequenceNumber()
-                + " and result: " + result);
+    Log.d(
+        TAG,
+        "Successfully completed reliable message transaction with "
+            + "message sequence number = "
+            + transaction.getMessageSequenceNumber()
+            + " and result = "
+            + result);
     }
 
     /**
@@ -732,15 +736,20 @@
         int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
         @ContextHubTransaction.Result int result = transaction.onTransact();
         if (result == ContextHubTransaction.RESULT_SUCCESS) {
-            Log.d(TAG, "Successfully "
-                    + (numCompletedStartCalls == 0 ? "started" : "retried")
-                    + " reliable message transaction with message sequence number: "
-                    + transaction.getMessageSequenceNumber());
+      Log.d(
+          TAG,
+          "Successfully "
+              + (numCompletedStartCalls == 0 ? "started" : "retried")
+              + " reliable message transaction with message sequence number = "
+              + transaction.getMessageSequenceNumber());
         } else {
-            Log.w(TAG, "Could not start reliable message transaction with "
-                    + "message sequence number: "
-                    + transaction.getMessageSequenceNumber()
-                    + ", result: " + result);
+      Log.w(
+          TAG,
+          "Could not start reliable message transaction with "
+              + "message sequence number = "
+              + transaction.getMessageSequenceNumber()
+              + ", result = "
+              + result);
         }
 
         transaction.setNextRetryTime(now + RELIABLE_MESSAGE_RETRY_WAIT_TIME.toNanos());
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index 5df0de8..df45a6e 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -77,7 +77,7 @@
                         mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
                                 intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
                         dispatchEmergencyStateChanged();
-                    } catch (IllegalStateException e) {
+                    } catch (IllegalStateException | UnsupportedOperationException e) {
                         Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
                     }
                 }
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 5a9cf03..bd551fb 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -230,9 +230,17 @@
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET),
-                    record -> mPackageManager.checkPermission(
+                    record -> {
+                        final String category = record.getNotification().category;
+                        if (Notification.CATEGORY_ALARM.equals(category)
+                                || Notification.CATEGORY_CAR_EMERGENCY.equals(category)
+                                || Notification.CATEGORY_CAR_WARNING.equals(category)) {
+                            return true;
+                        }
+                        return mPackageManager.checkPermission(
                             permission.RECEIVE_EMERGENCY_BROADCAST,
-                            record.getSbn().getPackageName()) == PERMISSION_GRANTED);
+                            record.getSbn().getPackageName()) == PERMISSION_GRANTED;
+                    });
 
             return new StrategyAvalanche(
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
@@ -248,9 +256,17 @@
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET),
-                    record -> mPackageManager.checkPermission(
+                    record -> {
+                        final String category = record.getNotification().category;
+                        if (Notification.CATEGORY_ALARM.equals(category)
+                                || Notification.CATEGORY_CAR_EMERGENCY.equals(category)
+                                || Notification.CATEGORY_CAR_WARNING.equals(category)) {
+                            return true;
+                        }
+                        return mPackageManager.checkPermission(
                             permission.RECEIVE_EMERGENCY_BROADCAST,
-                            record.getSbn().getPackageName()) == PERMISSION_GRANTED);
+                            record.getSbn().getPackageName()) == PERMISSION_GRANTED;
+                    });
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 0e7ce2e..14b0fc8 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -497,6 +497,17 @@
     public abstract boolean isUserVisible(@UserIdInt int userId, int displayId);
 
     /**
+     * Checks if the given user is a visible background full user, which is a full background user
+     * assigned to secondary displays on the devices that have
+     * {@link UserManager#isVisibleBackgroundUsersEnabled()
+     * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on
+     * automotive builds, using the display associated with their seats).
+     *
+     * @see UserManager#isUserVisible()
+     */
+    public abstract boolean isVisibleBackgroundFullUser(@UserIdInt int userId);
+
+    /**
      * Returns the main display id assigned to the user, or {@code Display.INVALID_DISPLAY} if the
      * user is not assigned to any main display.
      *
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index dde9943..c902fb2 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -7927,6 +7927,17 @@
         }
 
         @Override
+        public boolean isVisibleBackgroundFullUser(@UserIdInt int userId) {
+            if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+                return false;
+            }
+            boolean isForeground = userId == getCurrentUserId();
+            boolean isProfile = isProfileUnchecked(userId);
+            boolean isVisible = isUserVisible(userId);
+            return isVisible && !isForeground && !isProfile;
+        }
+
+        @Override
         public int getMainDisplayAssignedToUser(@UserIdInt int userId) {
             return mUserVisibilityMediator.getMainDisplayAssignedToUser(userId);
         }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c95be17..21d6c64 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2725,11 +2725,16 @@
 
         @Override
         void onLongPress(long eventTime) {
-            // Long-press should be triggered only if app doesn't handle it.
-            mDeferredKeyActionExecutor.queueKeyAction(
-                    KeyEvent.KEYCODE_STEM_PRIMARY,
-                    eventTime,
-                    () -> stemPrimaryLongPress(eventTime));
+            if (mLongPressOnStemPrimaryBehavior == LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT) {
+                // Long-press to assistant gesture is not overridable by apps.
+                stemPrimaryLongPress(eventTime);
+            } else {
+                // Other long-press actions should be triggered only if app doesn't handle it.
+                mDeferredKeyActionExecutor.queueKeyAction(
+                        KeyEvent.KEYCODE_STEM_PRIMARY,
+                        eventTime,
+                        () -> stemPrimaryLongPress(eventTime));
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 143b3ff..c878f14 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -16393,6 +16393,10 @@
      * Callers will need to wait for the collection to complete on the handler thread.
      */
     public void schedulePowerStatsSampleCollection() {
+        if (!mSystemReady) {
+            return;
+        }
+
         mCpuPowerStatsCollector.forceSchedule();
         mScreenPowerStatsCollector.forceSchedule();
         mMobileRadioPowerStatsCollector.forceSchedule();
@@ -16400,6 +16404,7 @@
         mBluetoothPowerStatsCollector.forceSchedule();
         mCameraPowerStatsCollector.forceSchedule();
         mGnssPowerStatsCollector.forceSchedule();
+        mCustomEnergyConsumerPowerStatsCollector.forceSchedule();
     }
 
     /**
diff --git a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
index 0273ba6..4bfe442 100644
--- a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
@@ -68,6 +68,16 @@
     }
 
     @Override
+    public boolean forceSchedule() {
+        ensureInitialized();
+        boolean success = false;
+        for (int i = 0; i < mCollectors.size(); i++) {
+            success |= mCollectors.get(i).forceSchedule();
+        }
+        return success;
+    }
+
+    @Override
     public void collectAndDump(PrintWriter pw) {
         ensureInitialized();
         for (int i = 0; i < mCollectors.size(); i++) {
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
index ce11fa0..79fbe8e 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
@@ -163,10 +163,7 @@
 
         mLayout.setConsumedEnergy(mPowerStats.stats, 0, uJtoUc(energyDelta, averageVoltage));
 
-        for (int i = mPowerStats.uidStats.size() - 1; i >= 0; i--) {
-            mLayout.setUidConsumedEnergy(mPowerStats.uidStats.valueAt(i), 0, 0);
-        }
-
+        mPowerStats.uidStats.clear();
         if (energy != null) {
             for (int i = energy.length - 1; i >= 0; i--) {
                 EnergyConsumerAttribution[] perUid = energy[i].attribution;
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
index 5f41090..bd75faa 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -216,6 +216,8 @@
             PowerStatsLayout layout) {
         AggregatedPowerStatsConfig.PowerComponent powerComponent = powerComponentStats.getConfig();
         int powerComponentId = powerComponent.getPowerComponentId();
+        boolean isCustomComponent =
+                powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
         PowerStats.Descriptor descriptor = powerComponentStats.getPowerStatsDescriptor();
         long[] uidStats = new long[descriptor.uidStatsArrayLength];
 
@@ -223,7 +225,7 @@
         boolean breakDownByProcState = batteryUsageStatsBuilder.isProcessStateDataNeeded()
                 && powerComponent
                 .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked()
-                && powerComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+                && !isCustomComponent;
 
         ArrayList<Integer> uids = new ArrayList<>();
         powerComponentStats.collectUids(uids);
@@ -237,7 +239,7 @@
             }
 
             for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) {
-                if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
+                if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) {
                     if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
                         continue;
                     }
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index c21f783..331a594 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -1301,7 +1301,7 @@
                 final NetworkStats stats = getUidNetworkStatsSnapshotForTemplateLocked(
                         new NetworkTemplate.Builder(MATCH_PROXY).build(),  /*includeTags=*/false);
                 if (stats != null) {
-                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats),
+                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
                             new int[]{TRANSPORT_BLUETOOTH},
                             /*slicedByFgbg=*/true, /*slicedByTag=*/false,
                             /*slicedByMetered=*/false, TelephonyManager.NETWORK_TYPE_UNKNOWN,
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 4085ec9..fb2bf39 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -1612,7 +1612,8 @@
 
         int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
         if (isAppCompateStateChangedToLetterboxed(state)) {
-            positionToLog = activity.mLetterboxUiController.getLetterboxPositionForLogging();
+            positionToLog = activity.mAppCompatController.getAppCompatReachabilityOverrides()
+                    .getLetterboxPositionForLogging();
         }
         FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED,
                 packageUid, state, positionToLog);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 8a768c0..eea3ab8 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8633,8 +8633,8 @@
     /**
      * Adjusts position of resolved bounds if they don't fill the parent using gravity
      * requested in the config or via an ADB command. For more context see {@link
-     * LetterboxUiController#getHorizontalPositionMultiplier(Configuration)} and
-     * {@link LetterboxUiController#getVerticalPositionMultiplier(Configuration)}
+     * AppCompatReachabilityOverrides#getHorizontalPositionMultiplier(Configuration)} and
+     * {@link AppCompatReachabilityOverrides#getVerticalPositionMultiplier(Configuration)}
      * <p>
      * Note that this is the final step that can change the resolved bounds. After this method
      * is called, the position of the bounds will be moved to app space as sandboxing if the
@@ -8663,11 +8663,13 @@
         } else {
             navBarInsets = Insets.NONE;
         }
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mAppCompatController.getAppCompatReachabilityOverrides();
         // Horizontal position
         int offsetX = 0;
         if (parentBounds.width() != screenResolvedBoundsWidth) {
             if (screenResolvedBoundsWidth <= parentAppBoundsWidth) {
-                float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
+                float positionMultiplier = reachabilityOverrides.getHorizontalPositionMultiplier(
                         newParentConfiguration);
                 // If in immersive mode, always align to right and overlap right insets (task bar)
                 // as they are transient and hidden. This removes awkward right spacing.
@@ -8688,7 +8690,7 @@
         int offsetY = 0;
         if (parentBoundsHeight != screenResolvedBoundsHeight) {
             if (screenResolvedBoundsHeight <= parentAppBoundsHeight) {
-                float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
+                float positionMultiplier = reachabilityOverrides.getVerticalPositionMultiplier(
                         newParentConfiguration);
                 // If in immersive mode, always align to bottom and overlap bottom insets (nav bar,
                 // task bar) as they are transient and hidden. This removes awkward bottom spacing.
@@ -10686,6 +10688,9 @@
                 return true;
             }
         }
+        if (mAtmService.mBackNavigationController.isStartingSurfaceShown(this)) {
+            return true;
+        }
         if (!super.isSyncFinished(group)) return false;
         if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController
                 .isVisibilityUnknown(this)) {
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index aa63393..24ed1bb 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -23,7 +23,6 @@
 import android.app.ActivityManager;
 import android.graphics.Rect;
 import android.os.Environment;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.ArraySet;
 import android.util.IntArray;
@@ -33,7 +32,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.window.flags.Flags;
 
 import java.io.File;
 import java.io.PrintWriter;
@@ -109,7 +107,6 @@
                 !service.mContext
                         .getResources()
                         .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots)
-                && isSnapshotEnabled()
                 && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go
         setSnapshotEnabled(snapshotEnabled);
     }
@@ -121,12 +118,6 @@
         return Math.max(Math.min(config, 1f), 0.1f);
     }
 
-    // TODO remove when enabled
-    static boolean isSnapshotEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0
-                || Flags.activitySnapshotByDefault();
-    }
-
     static PersistInfoProvider createPersistInfoProvider(
             WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) {
         // Don't persist reduced file, instead we only persist the "HighRes" bitmap which has
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 1c14c5d..9f3bbd1 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -7175,25 +7175,6 @@
         }
 
         /**
-         * Checks if the given user is a visible background user, which is a full, background user
-         * assigned to secondary displays on the devices that have
-         * {@link UserManager#isVisibleBackgroundUsersEnabled()
-         * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on
-         * automotive builds, using the display associated with their seats).
-         *
-         * @see UserManager#isUserVisible()
-         */
-        private boolean isVisibleBackgroundUser(int userId) {
-            if (!UserManager.isVisibleBackgroundUsersEnabled()) {
-                return false;
-            }
-            boolean isForeground = getCurrentUserId() == userId;
-            boolean isProfile = getUserManager().isProfile(userId);
-            boolean isVisible = mWindowManager.mUmInternal.isUserVisible(userId);
-            return isVisible && !isForeground && !isProfile;
-        }
-
-        /**
          * In a car environment, {@link ActivityTaskManagerService#mShowDialogs} is always set to
          * {@code false} from {@link ActivityTaskManagerService#updateShouldShowDialogsLocked}
          * because its UI mode is {@link Configuration#UI_MODE_TYPE_CAR}. Thus, error dialogs are
@@ -7208,7 +7189,7 @@
          * @see ActivityTaskManagerService#updateShouldShowDialogsLocked
          */
         private boolean shouldShowDialogsForVisibleBackgroundUserLocked(int userId) {
-            if (!isVisibleBackgroundUser(userId)) {
+            if (!mWindowManager.mUmInternal.isVisibleBackgroundFullUser(userId)) {
                 return false;
             }
             final int displayId = mWindowManager.mUmInternal.getMainDisplayAssignedToUser(userId);
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index 25cb134..d2f3d1d 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -50,8 +50,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.utils.OptPropFactory;
 
-import java.util.function.Function;
-
 /**
  * Encapsulates app compat configurations and overrides related to aspect ratio.
  */
@@ -76,20 +74,20 @@
     @NonNull
     private final OptPropFactory.OptProp mAllowOrientationOverrideOptProp;
     @NonNull
-    private final Function<Boolean, Boolean> mIsDisplayFullScreenAndInPostureProvider;
+    private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery;
     @NonNull
-    private final Function<Configuration, Float> mGetHorizontalPositionMultiplierProvider;
+    private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
 
     AppCompatAspectRatioOverrides(@NonNull ActivityRecord activityRecord,
             @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder,
-            @NonNull Function<Boolean, Boolean> isDisplayFullScreenAndInPostureProvider,
-            @NonNull Function<Configuration, Float> getHorizontalPositionMultiplierProvider) {
+            @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery,
+            @NonNull AppCompatReachabilityOverrides appCompatReachabilityOverrides) {
         mActivityRecord = activityRecord;
         mAppCompatConfiguration = appCompatConfiguration;
+        mAppCompatDeviceStateQuery = appCompatDeviceStateQuery;
         mUserAspectRatioState = new UserAspectRatioState();
-        mIsDisplayFullScreenAndInPostureProvider = isDisplayFullScreenAndInPostureProvider;
-        mGetHorizontalPositionMultiplierProvider = getHorizontalPositionMultiplierProvider;
+        mAppCompatReachabilityOverrides = appCompatReachabilityOverrides;
         mAllowMinAspectRatioOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
         mAllowUserAspectRatioOverrideOptProp = optPropBuilder.create(
@@ -245,12 +243,13 @@
     }
 
     private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
-        final boolean isBookMode = mIsDisplayFullScreenAndInPostureProvider
-                .apply(/* isTabletop */false);
-        final boolean isNotCenteredHorizontally = mGetHorizontalPositionMultiplierProvider.apply(
-                parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
-        final boolean isTabletopMode = mIsDisplayFullScreenAndInPostureProvider
-                .apply(/* isTabletop */ true);
+        final boolean isBookMode = mAppCompatDeviceStateQuery
+                .isDisplayFullScreenAndInPosture(/* isTabletop */false);
+        final boolean isNotCenteredHorizontally =
+                mAppCompatReachabilityOverrides.getHorizontalPositionMultiplier(parentConfiguration)
+                        != LETTERBOX_POSITION_MULTIPLIER_CENTER;
+        final boolean isTabletopMode = mAppCompatDeviceStateQuery
+                .isDisplayFullScreenAndInPosture(/* isTabletop */ true);
         final boolean isLandscape = isFixedOrientationLandscape(
                 mActivityRecord.getOverrideOrientation());
         final AppCompatCameraOverrides cameraOverrides =
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 54223b6..d38edfc 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -35,7 +35,11 @@
     @NonNull
     private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy;
     @NonNull
+    private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+    @NonNull
     private final AppCompatOverrides mAppCompatOverrides;
+    @NonNull
+    private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery;
 
     AppCompatController(@NonNull WindowManagerService wmService,
                         @NonNull ActivityRecord activityRecord) {
@@ -43,13 +47,16 @@
         final PackageManager packageManager = wmService.mContext.getPackageManager();
         final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
                 activityRecord.packageName);
+        mAppCompatDeviceStateQuery = new AppCompatDeviceStateQuery(activityRecord);
         mTransparentPolicy = new TransparentPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
         mAppCompatOverrides = new AppCompatOverrides(activityRecord,
-                wmService.mAppCompatConfiguration, optPropBuilder);
+                wmService.mAppCompatConfiguration, optPropBuilder, mAppCompatDeviceStateQuery);
         mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides);
         mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord,
                 mTransparentPolicy, mAppCompatOverrides);
+        mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord,
+                wmService.mAppCompatConfiguration);
     }
 
     @NonNull
@@ -101,7 +108,23 @@
     }
 
     @NonNull
+    AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
+        return mAppCompatReachabilityPolicy;
+    }
+
+    @NonNull
     AppCompatFocusOverrides getAppCompatFocusOverrides() {
         return mAppCompatOverrides.getAppCompatFocusOverrides();
     }
+
+    @NonNull
+    AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
+        return mAppCompatOverrides.getAppCompatReachabilityOverrides();
+    }
+
+    @NonNull
+    AppCompatDeviceStateQuery getAppCompatDeviceStateQuery() {
+        return mAppCompatDeviceStateQuery;
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatDeviceStateQuery.java b/services/core/java/com/android/server/wm/AppCompatDeviceStateQuery.java
new file mode 100644
index 0000000..3abea24
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatDeviceStateQuery.java
@@ -0,0 +1,64 @@
+/*
+ * 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.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.annotation.NonNull;
+
+/**
+ * Provides information about the current state of the display in relation of
+ * fold/unfold and other positions.
+ */
+class AppCompatDeviceStateQuery {
+
+    @NonNull
+    final ActivityRecord mActivityRecord;
+
+    AppCompatDeviceStateQuery(@NonNull ActivityRecord activityRecord) {
+        mActivityRecord = activityRecord;
+    }
+
+    /**
+     * Check if we are in the given pose and in fullscreen mode.
+     *
+     * Note that we check the task rather than the parent as with ActivityEmbedding the parent
+     * might be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+     * actually fullscreen. If display is still in transition e.g. unfolding, don't return true
+     * for HALF_FOLDED state or app will flicker.
+     */
+    boolean isDisplayFullScreenAndInPosture(boolean isTabletop) {
+        final Task task = mActivityRecord.getTask();
+        final DisplayContent dc = mActivityRecord.mDisplayContent;
+        return dc != null && task != null && !dc.inTransition()
+                && dc.getDisplayRotation().isDeviceInPosture(
+                    DeviceStateController.DeviceState.HALF_FOLDED, isTabletop)
+                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    }
+
+    /**
+     * Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+     * be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+     * actually fullscreen.
+     */
+    boolean isDisplayFullScreenAndSeparatingHinge() {
+        final Task task = mActivityRecord.getTask();
+        return mActivityRecord.mDisplayContent != null && task != null
+                && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
+                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index 4450011..80bbee3 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -35,19 +35,22 @@
     private final AppCompatFocusOverrides mAppCompatFocusOverrides;
     @NonNull
     private final AppCompatResizeOverrides mAppCompatResizeOverrides;
+    @NonNull
+    private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
 
     AppCompatOverrides(@NonNull ActivityRecord activityRecord,
             @NonNull AppCompatConfiguration appCompatConfiguration,
-            @NonNull OptPropFactory optPropBuilder) {
+            @NonNull OptPropFactory optPropBuilder,
+            @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) {
         mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder);
         mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides);
-        // TODO(b/341903757) Remove BooleanSuppliers after fixing dependency with reachability.
+        mAppCompatReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord,
+                appCompatConfiguration, appCompatDeviceStateQuery);
         mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord,
-                appCompatConfiguration, optPropBuilder,
-                activityRecord.mLetterboxUiController::isDisplayFullScreenAndInPosture,
-                activityRecord.mLetterboxUiController::getHorizontalPositionMultiplier);
+                appCompatConfiguration, optPropBuilder, appCompatDeviceStateQuery,
+                mAppCompatReachabilityOverrides);
         mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder);
         mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder);
@@ -77,4 +80,9 @@
     AppCompatResizeOverrides getAppCompatResizeOverrides() {
         return mAppCompatResizeOverrides;
     }
+
+    @NonNull
+    AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
+        return mAppCompatReachabilityOverrides;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java
new file mode 100644
index 0000000..b9bdc32
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java
@@ -0,0 +1,343 @@
+/*
+ * 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.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+
+import android.annotation.NonNull;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
+/**
+ * Encapsulate overrides and configurations about app compat reachability.
+ */
+class AppCompatReachabilityOverrides {
+
+    @NonNull
+    private final ActivityRecord mActivityRecord;
+    @NonNull
+    private final AppCompatConfiguration mAppCompatConfiguration;
+    @NonNull
+    private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery;
+    @NonNull
+    private final ReachabilityState mReachabilityState;
+
+    AppCompatReachabilityOverrides(@NonNull ActivityRecord activityRecord,
+            @NonNull AppCompatConfiguration appCompatConfiguration,
+            @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) {
+        mActivityRecord = activityRecord;
+        mAppCompatConfiguration = appCompatConfiguration;
+        mAppCompatDeviceStateQuery = appCompatDeviceStateQuery;
+        mReachabilityState = new ReachabilityState();
+    }
+
+    boolean isFromDoubleTap() {
+        return mReachabilityState.isFromDoubleTap();
+    }
+
+    boolean isDoubleTapEvent() {
+        return mReachabilityState.mIsDoubleTapEvent;
+    }
+
+    void setDoubleTapEvent() {
+        mReachabilityState.mIsDoubleTapEvent = true;
+    }
+
+    /**
+     * Provides the multiplier to use when calculating the position of a letterboxed app after
+     * an horizontal reachability event (double tap). The method takes the current state of the
+     * device (e.g. device in book mode) into account.
+     * </p>
+     * @param parentConfiguration The parent {@link Configuration}.
+     * @return The value to use for calculating the letterbox horizontal position.
+     */
+    float getHorizontalPositionMultiplier(@NonNull Configuration parentConfiguration) {
+        // Don't check resolved configuration because it may not be updated yet during
+        // configuration change.
+        boolean bookModeEnabled = isFullScreenAndBookModeEnabled();
+        return isHorizontalReachabilityEnabled(parentConfiguration)
+                // Using the last global dynamic position to avoid "jumps" when moving
+                // between apps or activities.
+                ? mAppCompatConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
+                : mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
+    }
+
+    /**
+     * Provides the multiplier to use when calculating the position of a letterboxed app after
+     * a vertical reachability event (double tap). The method takes the current state of the
+     * device (e.g. device posture) into account.
+     * </p>
+     * @param parentConfiguration The parent {@link Configuration}.
+     * @return The value to use for calculating the letterbox horizontal position.
+     */
+    float getVerticalPositionMultiplier(@NonNull Configuration parentConfiguration) {
+        // Don't check resolved configuration because it may not be updated yet during
+        // configuration change.
+        boolean tabletopMode = mAppCompatDeviceStateQuery
+                .isDisplayFullScreenAndInPosture(/* isTabletop */ true);
+        return isVerticalReachabilityEnabled(parentConfiguration)
+                // Using the last global dynamic position to avoid "jumps" when moving
+                // between apps or activities.
+                ? mAppCompatConfiguration.getVerticalMultiplierForReachability(tabletopMode)
+                : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
+    }
+
+    @VisibleForTesting
+    boolean isHorizontalReachabilityEnabled() {
+        return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
+    }
+
+    @VisibleForTesting
+    boolean isVerticalReachabilityEnabled() {
+        return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
+    }
+
+    boolean isLetterboxDoubleTapEducationEnabled() {
+        return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled();
+    }
+
+    @AppCompatConfiguration.LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        final boolean isInFullScreenTabletopMode =
+                mAppCompatDeviceStateQuery.isDisplayFullScreenAndSeparatingHinge();
+        return mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(
+                isInFullScreenTabletopMode);
+    }
+
+    @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
+        return mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(
+                isInFullScreenBookMode);
+    }
+
+    int getLetterboxPositionForLogging() {
+        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+        if (isHorizontalReachabilityEnabled()) {
+            int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
+                    .getLetterboxPositionForHorizontalReachability(mAppCompatDeviceStateQuery
+                            .isDisplayFullScreenAndInPosture(/* isTabletop */ false));
+            positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPositionForLogging(
+                    letterboxPositionForHorizontalReachability);
+        } else if (isVerticalReachabilityEnabled()) {
+            int letterboxPositionForVerticalReachability = mAppCompatConfiguration
+                    .getLetterboxPositionForVerticalReachability(mAppCompatDeviceStateQuery
+                            .isDisplayFullScreenAndInPosture(/* isTabletop */ true));
+            positionToLog = letterboxVerticalReachabilityPositionToLetterboxPositionForLogging(
+                    letterboxPositionForVerticalReachability);
+        }
+        return positionToLog;
+    }
+
+    /**
+     * @return {@value true} if the vertical reachability should be allowed in case of
+     * thin letterboxing.
+     */
+    boolean allowVerticalReachabilityForThinLetterbox() {
+        if (!Flags.disableThinLetterboxingPolicy()) {
+            return true;
+        }
+        // When the flag is enabled we allow vertical reachability only if the
+        // app is not thin letterboxed vertically.
+        return !isVerticalThinLetterboxed();
+    }
+
+    /**
+     * @return {@value true} if the horizontal reachability should be enabled in case of
+     * thin letterboxing.
+     */
+    boolean allowHorizontalReachabilityForThinLetterbox() {
+        if (!Flags.disableThinLetterboxingPolicy()) {
+            return true;
+        }
+        // When the flag is enabled we allow horizontal reachability only if the
+        // app is not thin pillarboxed.
+        return !isHorizontalThinLetterboxed();
+    }
+
+    /**
+     * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
+     */
+    boolean isVerticalThinLetterboxed() {
+        final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx();
+        if (thinHeight < 0) {
+            return false;
+        }
+        final Task task = mActivityRecord.getTask();
+        if (task == null) {
+            return false;
+        }
+        final int padding = Math.abs(
+                task.getBounds().height() - mActivityRecord.getBounds().height()) / 2;
+        return padding <= thinHeight;
+    }
+
+    /**
+     * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
+     */
+    boolean isHorizontalThinLetterboxed() {
+        final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx();
+        if (thinWidth < 0) {
+            return false;
+        }
+        final Task task = mActivityRecord.getTask();
+        if (task == null) {
+            return false;
+        }
+        final int padding = Math.abs(
+                task.getBounds().width() - mActivityRecord.getBounds().width()) / 2;
+        return padding <= thinWidth;
+    }
+
+    // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+    // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+    // actually fullscreen.
+    private boolean isDisplayFullScreenAndSeparatingHinge() {
+        Task task = mActivityRecord.getTask();
+        return mActivityRecord.mDisplayContent != null
+                && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
+                && task != null
+                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    }
+
+    private int letterboxHorizontalReachabilityPositionToLetterboxPositionForLogging(
+            @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox horizontal reachability position type: "
+                                + position);
+        }
+    }
+
+    private int letterboxVerticalReachabilityPositionToLetterboxPositionForLogging(
+            @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox vertical reachability position type: "
+                                + position);
+        }
+    }
+
+    private boolean isFullScreenAndBookModeEnabled() {
+        return mAppCompatDeviceStateQuery.isDisplayFullScreenAndInPosture(/* isTabletop */ false)
+                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
+    }
+
+    /**
+     * Whether horizontal reachability is enabled for an activity in the current configuration.
+     *
+     * <p>Conditions that needs to be met:
+     * <ul>
+     *   <li>Windowing mode is fullscreen.
+     *   <li>Horizontal Reachability is enabled.
+     *   <li>First top opaque activity fills parent vertically, but not horizontally.
+     * </ul>
+     */
+    private boolean isHorizontalReachabilityEnabled(@NonNull Configuration parentConfiguration) {
+        if (!allowHorizontalReachabilityForThinLetterbox()) {
+            return false;
+        }
+        final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
+        final Rect parentAppBounds = parentAppBoundsOverride != null
+                ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
+        // Use screen resolved bounds which uses resolved bounds or size compat bounds
+        // as activity bounds can sometimes be empty
+        final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController
+                .getTransparentPolicy().getFirstOpaqueActivity()
+                .map(ActivityRecord::getScreenResolvedBounds)
+                .orElse(mActivityRecord.getScreenResolvedBounds());
+        return mAppCompatConfiguration.getIsHorizontalReachabilityEnabled()
+                && parentConfiguration.windowConfiguration.getWindowingMode()
+                == WINDOWING_MODE_FULLSCREEN
+                // Check whether the activity fills the parent vertically.
+                && parentAppBounds.height() <= opaqueActivityBounds.height()
+                && parentAppBounds.width() > opaqueActivityBounds.width();
+    }
+
+    /**
+     * Whether vertical reachability is enabled for an activity in the current configuration.
+     *
+     * <p>Conditions that needs to be met:
+     * <ul>
+     *   <li>Windowing mode is fullscreen.
+     *   <li>Vertical Reachability is enabled.
+     *   <li>First top opaque activity fills parent horizontally but not vertically.
+     * </ul>
+     */
+    private boolean isVerticalReachabilityEnabled(@NonNull Configuration parentConfiguration) {
+        if (!allowVerticalReachabilityForThinLetterbox()) {
+            return false;
+        }
+        final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
+        final Rect parentAppBounds = parentAppBoundsOverride != null
+                ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
+        // Use screen resolved bounds which uses resolved bounds or size compat bounds
+        // as activity bounds can sometimes be empty.
+        final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController
+                .getTransparentPolicy().getFirstOpaqueActivity()
+                .map(ActivityRecord::getScreenResolvedBounds)
+                .orElse(mActivityRecord.getScreenResolvedBounds());
+        return mAppCompatConfiguration.getIsVerticalReachabilityEnabled()
+                && parentConfiguration.windowConfiguration.getWindowingMode()
+                    == WINDOWING_MODE_FULLSCREEN
+                // Check whether the activity fills the parent horizontally.
+                && parentAppBounds.width() <= opaqueActivityBounds.width()
+                && parentAppBounds.height() > opaqueActivityBounds.height();
+    }
+
+    private static class ReachabilityState {
+        // If the current event is a double tap.
+        private boolean mIsDoubleTapEvent;
+
+        boolean isFromDoubleTap() {
+            final boolean isFromDoubleTap = mIsDoubleTapEvent;
+            mIsDoubleTapEvent = false;
+            return isFromDoubleTap;
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
new file mode 100644
index 0000000..e4e7654
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
@@ -0,0 +1,153 @@
+/*
+ * 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.server.wm;
+
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.function.Supplier;
+
+/**
+ * Encapsulate logic about app compat reachability.
+ */
+class AppCompatReachabilityPolicy {
+
+    @NonNull
+    private final ActivityRecord mActivityRecord;
+    @NonNull
+    private final AppCompatConfiguration mAppCompatConfiguration;
+
+    AppCompatReachabilityPolicy(@NonNull ActivityRecord activityRecord,
+            @NonNull AppCompatConfiguration appCompatConfiguration) {
+        mActivityRecord = activityRecord;
+        mAppCompatConfiguration = appCompatConfiguration;
+    }
+
+    @VisibleForTesting
+    void handleHorizontalDoubleTap(int x, @NonNull Supplier<Rect> innerFrameSupplier) {
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides();
+        if (!reachabilityOverrides.isHorizontalReachabilityEnabled()
+                || mActivityRecord.isInTransition()) {
+            return;
+        }
+        final Rect letterboxInnerFrame = innerFrameSupplier.get();
+        if (letterboxInnerFrame.left <= x && letterboxInnerFrame.right >= x) {
+            // Only react to clicks at the sides of the letterboxed app window.
+            return;
+        }
+        final AppCompatDeviceStateQuery deviceStateQuery = mActivityRecord.mAppCompatController
+                .getAppCompatDeviceStateQuery();
+        final boolean isInFullScreenBookMode = deviceStateQuery
+                    .isDisplayFullScreenAndSeparatingHinge()
+                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
+        final int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
+                .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
+        if (letterboxInnerFrame.left > x) {
+            // Moving to the next stop on the left side of the app window: right > center > left.
+            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
+                    isInFullScreenBookMode);
+            int letterboxPositionChangeForLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+            logLetterboxPositionChange(letterboxPositionChangeForLog);
+            reachabilityOverrides.setDoubleTapEvent();
+        } else if (letterboxInnerFrame.right < x) {
+            // Moving to the next stop on the right side of the app window: left > center > right.
+            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+                    isInFullScreenBookMode);
+            final int letterboxPositionChangeForLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+            logLetterboxPositionChange(letterboxPositionChangeForLog);
+            reachabilityOverrides.setDoubleTapEvent();
+        }
+        // TODO(197549949): Add animation for transition.
+        mActivityRecord.recomputeConfiguration();
+    }
+
+    @VisibleForTesting
+    void handleVerticalDoubleTap(int y, @NonNull Supplier<Rect> innerFrameSupplier) {
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides();
+        if (!reachabilityOverrides.isVerticalReachabilityEnabled()
+                || mActivityRecord.isInTransition()) {
+            return;
+        }
+        final Rect letterboxInnerFrame = innerFrameSupplier.get();
+        if (letterboxInnerFrame.top <= y && letterboxInnerFrame.bottom >= y) {
+            // Only react to clicks at the top and bottom of the letterboxed app window.
+            return;
+        }
+        final AppCompatDeviceStateQuery deviceStateQuery = mActivityRecord.mAppCompatController
+                .getAppCompatDeviceStateQuery();
+        final boolean isInFullScreenTabletopMode = deviceStateQuery
+                .isDisplayFullScreenAndSeparatingHinge();
+        final int letterboxPositionForVerticalReachability = mAppCompatConfiguration
+                .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
+        if (letterboxInnerFrame.top > y) {
+            // Moving to the next stop on the top side of the app window: bottom > center > top.
+            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextTopStop(
+                    isInFullScreenTabletopMode);
+            final int letterboxPositionChangeForLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+            logLetterboxPositionChange(letterboxPositionChangeForLog);
+            reachabilityOverrides.setDoubleTapEvent();
+        } else if (letterboxInnerFrame.bottom < y) {
+            // Moving to the next stop on the bottom side of the app window: top > center > bottom.
+            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+                    isInFullScreenTabletopMode);
+            final int letterboxPositionChangeForLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
+            logLetterboxPositionChange(letterboxPositionChangeForLog);
+            reachabilityOverrides.setDoubleTapEvent();
+        }
+        // TODO(197549949): Add animation for transition.
+        mActivityRecord.recomputeConfiguration();
+    }
+
+    /**
+     * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
+     */
+    private void logLetterboxPositionChange(int letterboxPositionChangeForLog) {
+        mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
+                .logLetterboxPositionChange(mActivityRecord, letterboxPositionChangeForLog);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index a4cb389..8c5193e 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -98,11 +98,11 @@
         appCompatTaskInfo.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET;
         appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode =
                 CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
-
         if (top == null) {
             return;
         }
-
+        final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController
+                .getAppCompatReachabilityOverrides();
         final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED);
         final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible();
         // Whether the direct top activity is in size compat mode.
@@ -123,30 +123,27 @@
         appCompatTaskInfo.isSystemFullscreenOverrideEnabled = top.mAppCompatController
                 .getAppCompatAspectRatioOverrides().isSystemOverrideToFullscreenEnabled();
 
-        appCompatTaskInfo.isFromLetterboxDoubleTap = top.mLetterboxUiController.isFromDoubleTap();
+        appCompatTaskInfo.isFromLetterboxDoubleTap = reachabilityOverrides.isFromDoubleTap();
         appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width();
         appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height();
-
         // We need to consider if letterboxed or pillarboxed.
         // TODO(b/336807329) Encapsulate reachability logic
-        appCompatTaskInfo.isLetterboxDoubleTapEnabled = top.mLetterboxUiController
+        appCompatTaskInfo.isLetterboxDoubleTapEnabled = reachabilityOverrides
                 .isLetterboxDoubleTapEducationEnabled();
         if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) {
             if (appCompatTaskInfo.isTopActivityPillarboxed()) {
-                if (top.mLetterboxUiController.allowHorizontalReachabilityForThinLetterbox()) {
+                if (reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()) {
                     // Pillarboxed.
                     appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
-                            top.mLetterboxUiController
-                                    .getLetterboxPositionForHorizontalReachability();
+                            reachabilityOverrides.getLetterboxPositionForHorizontalReachability();
                 } else {
                     appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
                 }
             } else {
-                if (top.mLetterboxUiController.allowVerticalReachabilityForThinLetterbox()) {
+                if (reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()) {
                     // Letterboxed.
                     appCompatTaskInfo.topActivityLetterboxVerticalPosition =
-                            top.mLetterboxUiController
-                                    .getLetterboxPositionForVerticalReachability();
+                            reachabilityOverrides.getLetterboxPositionForVerticalReachability();
                 } else {
                     appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
                 }
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 78636a7..5a0cbf3 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -250,7 +250,7 @@
 
         ArraySet<ActivityRecord> tmpOpenApps = mDisplayContent.mOpeningApps;
         ArraySet<ActivityRecord> tmpCloseApps = mDisplayContent.mClosingApps;
-        if (mDisplayContent.mAtmService.mBackNavigationController.isMonitoringTransition()) {
+        if (mDisplayContent.mAtmService.mBackNavigationController.isMonitoringFinishTransition()) {
             tmpOpenApps = new ArraySet<>(mDisplayContent.mOpeningApps);
             tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps);
             if (mDisplayContent.mAtmService.mBackNavigationController
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 0f8d68b..48e1079 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -24,6 +24,7 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_NONE;
+import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
@@ -47,6 +48,7 @@
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.RemoteAnimationTarget;
@@ -83,11 +85,6 @@
 
     private AnimationHandler mAnimationHandler;
 
-    /**
-     * The transition who match the back navigation targets,
-     * release animation after this transition finish.
-     */
-    private Transition mWaitTransitionFinish;
     private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>();
     private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>();
 
@@ -143,7 +140,7 @@
 
         BackNavigationInfo.Builder infoBuilder = new BackNavigationInfo.Builder();
         synchronized (wmService.mGlobalLock) {
-            if (isMonitoringTransition()) {
+            if (isMonitoringFinishTransition()) {
                 Slog.w(TAG, "Previous animation hasn't finish, status: " + mAnimationHandler);
                 // Don't start any animation for it.
                 return null;
@@ -308,6 +305,7 @@
                     backType = BackNavigationInfo.TYPE_CALLBACK;
                 } else if (prevTask.isActivityTypeHome()) {
                     removedWindowContainer = currentTask;
+                    prevTask = prevTask.getRootTask();
                     backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
                     final ActivityRecord ar = prevTask.getTopNonFinishingActivity();
                     mShowWallpaper = ar != null && ar.hasWallpaper();
@@ -562,10 +560,15 @@
         return !prevActivities.isEmpty();
     }
 
-    boolean isMonitoringTransition() {
+    boolean isMonitoringFinishTransition() {
         return mAnimationHandler.mComposed || mNavigationMonitor.isMonitorForRemote();
     }
 
+    boolean isMonitoringPrepareTransition(Transition transition) {
+        return mAnimationHandler.mComposed
+                && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition == transition;
+    }
+
     private void scheduleAnimation(@NonNull AnimationHandler.ScheduleAnimationBuilder builder) {
         mPendingAnimation = builder.build();
         mWindowManagerService.mWindowPlacerLocked.requestTraversal();
@@ -576,7 +579,9 @@
     }
 
     private boolean isWaitBackTransition() {
-        return mAnimationHandler.mComposed && mAnimationHandler.mWaitTransition;
+        // Ignore mWaitTransition while flag is enabled.
+        return mAnimationHandler.mComposed && (Flags.migratePredictiveBackTransition()
+                || mAnimationHandler.mWaitTransition);
     }
 
     boolean isKeyguardOccluded(WindowState focusWindow) {
@@ -626,7 +631,7 @@
      */
     boolean removeIfContainsBackAnimationTargets(ArraySet<ActivityRecord> openApps,
             ArraySet<ActivityRecord> closeApps) {
-        if (!isMonitoringTransition()) {
+        if (!isMonitoringFinishTransition()) {
             return false;
         }
         mTmpCloseApps.addAll(closeApps);
@@ -652,7 +657,6 @@
                 final ActivityRecord ar = openApps.valueAt(i);
                 if (mAnimationHandler.isTarget(ar, true /* open */)) {
                     openApps.removeAt(i);
-                    mAnimationHandler.markStartingSurfaceMatch(null /* reparentTransaction */);
                 }
             }
             for (int i = closeApps.size() - 1; i >= 0; --i) {
@@ -670,6 +674,12 @@
         mAnimationHandler.markWindowHasDrawn(openActivity);
     }
 
+    boolean isStartingSurfaceShown(ActivityRecord openActivity) {
+        if (!Flags.migratePredictiveBackTransition()) {
+            return false;
+        }
+        return mAnimationHandler.isStartingSurfaceDrawn(openActivity);
+    }
     @VisibleForTesting
     class NavigationMonitor {
         // The window which triggering the back navigation.
@@ -767,8 +777,14 @@
      * open or close list.
      */
     void onTransactionReady(Transition transition, ArrayList<Transition.ChangeInfo> targets,
-            SurfaceControl.Transaction startTransaction) {
-        if (!isMonitoringTransition() || targets.isEmpty()) {
+            SurfaceControl.Transaction startTransaction,
+            SurfaceControl.Transaction finishTransaction) {
+        if (isMonitoringPrepareTransition(transition)) {
+            // Flag target matches and prepare to remove windowless surface.
+            mAnimationHandler.markStartingSurfaceMatch(startTransaction);
+            return;
+        }
+        if (!isMonitoringFinishTransition() || targets.isEmpty()) {
             return;
         }
         if (mAnimationHandler.hasTargetDetached()) {
@@ -801,42 +817,40 @@
         if (!matchAnimationTargets) {
             mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps);
         } else {
-            if (mWaitTransitionFinish != null) {
+            if (mAnimationHandler.mPrepareCloseTransition != null) {
                 Slog.e(TAG, "Gesture animation is applied on another transition?");
             }
-            mWaitTransitionFinish = transition;
-            // Flag target matches to defer remove the splash screen.
-            for (int i = mTmpOpenApps.size() - 1; i >= 0; --i) {
-                final WindowContainer wc = mTmpOpenApps.get(i);
-                if (mAnimationHandler.isTarget(wc, true /* open */)) {
-                    mAnimationHandler.markStartingSurfaceMatch(startTransaction);
-                    break;
-                }
+            mAnimationHandler.mPrepareCloseTransition = transition;
+            if (!Flags.migratePredictiveBackTransition()) {
+                // Because the target will reparent to transition root, so it cannot be controlled
+                // by animation leash. Hide the close target when transition starts.
+                startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl());
             }
+            // Flag target matches and prepare to remove windowless surface.
+            mAnimationHandler.markStartingSurfaceMatch(startTransaction);
             // release animation leash
             if (mAnimationHandler.mOpenAnimAdaptor.mCloseTransaction != null) {
-                startTransaction.merge(mAnimationHandler.mOpenAnimAdaptor.mCloseTransaction);
+                finishTransaction.merge(mAnimationHandler.mOpenAnimAdaptor.mCloseTransaction);
                 mAnimationHandler.mOpenAnimAdaptor.mCloseTransaction = null;
             }
-            // Because the target will reparent to transition root, so it cannot be controlled by
-            // animation leash. Hide the close target when transition starts.
-            startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl());
         }
         mTmpOpenApps.clear();
         mTmpCloseApps.clear();
     }
 
     boolean isMonitorTransitionTarget(WindowContainer wc) {
-        if (!isWaitBackTransition() || mWaitTransitionFinish == null) {
-            return false;
+        if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null)
+                || (mAnimationHandler.mOpenAnimAdaptor != null
+                && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition != null)) {
+            return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */);
         }
-        return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */);
+        return false;
     }
 
     boolean shouldPauseTouch(WindowContainer wc) {
         // Once the close transition is ready, it means the onBackInvoked callback has invoked, and
         // app is ready to trigger next transition, no matter what it will be.
-        return mAnimationHandler.mComposed && mWaitTransitionFinish == null
+        return mAnimationHandler.mComposed && mAnimationHandler.mPrepareCloseTransition == null
                 && mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */);
     }
 
@@ -847,7 +861,6 @@
     void clearBackAnimations(boolean cancel) {
         mAnimationHandler.clearBackAnimateTarget(cancel);
         mNavigationMonitor.stopMonitorTransition();
-        mWaitTransitionFinish = null;
     }
 
     /**
@@ -858,7 +871,13 @@
     */
     void onTransitionFinish(ArrayList<Transition.ChangeInfo> targets,
             @NonNull Transition finishedTransition) {
-        if (finishedTransition == mWaitTransitionFinish) {
+        if (isMonitoringPrepareTransition(finishedTransition)) {
+            if (mAnimationHandler.mPrepareCloseTransition == null) {
+                clearBackAnimations(true /* cancel */);
+            }
+            return;
+        }
+        if (finishedTransition == mAnimationHandler.mPrepareCloseTransition) {
             clearBackAnimations(false /* cancel */);
         }
         if (!mBackAnimationInProgress || mPendingAnimationBuilder == null) {
@@ -938,13 +957,13 @@
         // the opening target like starting window do.
         private boolean mStartingSurfaceTargetMatch;
         private ActivityRecord[] mOpenActivities;
+        Transition mPrepareCloseTransition;
 
         AnimationHandler(WindowManagerService wms) {
             mWindowManagerService = wms;
             final Context context = wms.mContext;
             mShowWindowlessSurface = context.getResources().getBoolean(
-                    com.android.internal.R.bool.config_predictShowStartingSurface)
-                    && Flags.activitySnapshotByDefault();
+                    com.android.internal.R.bool.config_predictShowStartingSurface);
         }
         private static final int UNKNOWN = 0;
         private static final int TASK_SWITCH = 1;
@@ -982,6 +1001,12 @@
                 @NonNull ActivityRecord[] openingActivities)  {
             if (isActivitySwitch(close, open)) {
                 mSwitchType = ACTIVITY_SWITCH;
+                if (Flags.migratePredictiveBackTransition()) {
+                    final Pair<WindowContainer, WindowContainer[]> replaced =
+                            promoteToTFIfNeeded(close, open);
+                    close = replaced.first;
+                    open = replaced.second;
+                }
             } else if (isTaskSwitch(close, open)) {
                 mSwitchType = TASK_SWITCH;
             } else if (isDialogClose(close)) {
@@ -1019,6 +1044,34 @@
             mOpenActivities = openingActivities;
         }
 
+        private Pair<WindowContainer, WindowContainer[]> promoteToTFIfNeeded(
+                WindowContainer close, WindowContainer[] open) {
+            WindowContainer replaceClose = close;
+            TaskFragment closeTF = close.asActivityRecord().getTaskFragment();
+            if (closeTF != null && !closeTF.isEmbedded()) {
+                closeTF = null;
+            }
+            final WindowContainer[] replaceOpen = new WindowContainer[open.length];
+            if (open.length >= 2) { // Promote to TaskFragment
+                for (int i = open.length - 1; i >= 0; --i) {
+                    replaceOpen[i] = open[i].asActivityRecord().getTaskFragment();
+                    replaceClose = closeTF != null ? closeTF : close;
+                }
+            } else {
+                TaskFragment openTF = open[0].asActivityRecord().getTaskFragment();
+                if (openTF != null && !openTF.isEmbedded()) {
+                    openTF = null;
+                }
+                if (closeTF != openTF) {
+                    replaceOpen[0] = openTF != null ? openTF : open[0];
+                    replaceClose = closeTF != null ? closeTF : close;
+                } else {
+                    replaceOpen[0] = open[0];
+                }
+            }
+            return new Pair<>(replaceClose, replaceOpen);
+        }
+
         private boolean composeAnimations(@NonNull WindowContainer close,
                 @NonNull WindowContainer[] open, @NonNull ActivityRecord[] openingActivities) {
             if (mComposed || mWaitTransition) {
@@ -1080,7 +1133,8 @@
         }
 
         void markWindowHasDrawn(ActivityRecord activity) {
-            if (!mComposed || mWaitTransition) {
+            if (!mComposed || mWaitTransition || mOpenAnimAdaptor.mPreparedOpenTransition == null
+                    || mOpenAnimAdaptor.mRequestedStartingSurfaceId == INVALID_TASK_ID) {
                 return;
             }
             boolean allWindowDrawn = true;
@@ -1096,6 +1150,17 @@
             }
         }
 
+        boolean isStartingSurfaceDrawn(ActivityRecord activity) {
+            // Check whether we create windowless surface to prepare open transition
+            if (!mComposed || mOpenAnimAdaptor.mPreparedOpenTransition == null) {
+                return false;
+            }
+            if (isTarget(activity, true /* open */)) {
+                return mOpenAnimAdaptor.mStartingSurface != null;
+            }
+            return false;
+        }
+
         private static boolean isAnimateTarget(@NonNull WindowContainer window,
                 @NonNull WindowContainer animationTarget, int switchType) {
             if (switchType == TASK_SWITCH) {
@@ -1109,7 +1174,9 @@
                         && window.hasChild(animationTarget));
             } else if (switchType == ACTIVITY_SWITCH) {
                 return window == animationTarget
-                        || (window.asTaskFragment() != null && window.hasChild(animationTarget));
+                        || (window.asTaskFragment() != null && window.hasChild(animationTarget))
+                        || (animationTarget.asTaskFragment() != null
+                        && animationTarget.hasChild(window));
             }
             return false;
         }
@@ -1122,8 +1189,14 @@
                         resetActivity.mDisplayContent
                                 .continueUpdateOrientationForDiffOrienLaunchingApp();
                     }
+                    final Transition finishTransition =
+                            resetActivity.mTransitionController.mFinishingTransition;
+                    final boolean inFinishTransition = finishTransition != null
+                            && (mPrepareCloseTransition == finishTransition
+                            || (mOpenAnimAdaptor != null
+                            && mOpenAnimAdaptor.mPreparedOpenTransition == finishTransition));
                     if (resetActivity.mLaunchTaskBehind) {
-                        restoreLaunchBehind(resetActivity, cancel);
+                        restoreLaunchBehind(resetActivity, cancel, inFinishTransition);
                     }
                 }
             }
@@ -1137,12 +1210,25 @@
             }
         }
 
-        void markStartingSurfaceMatch(SurfaceControl.Transaction reparentTransaction) {
+        void markStartingSurfaceMatch(SurfaceControl.Transaction startTransaction) {
             if (mStartingSurfaceTargetMatch) {
                 return;
             }
             mStartingSurfaceTargetMatch = true;
-            mOpenAnimAdaptor.reparentWindowlessSurfaceToTarget(reparentTransaction);
+
+            if (mOpenAnimAdaptor.mRequestedStartingSurfaceId == INVALID_TASK_ID) {
+                return;
+            }
+            final SurfaceControl startingSurface = mOpenAnimAdaptor.mStartingSurface;
+            if (startingSurface != null && startingSurface.isValid()) {
+                startTransaction.addTransactionCommittedListener(Runnable::run, () -> {
+                    synchronized (mWindowManagerService.mGlobalLock) {
+                        if (mOpenAnimAdaptor != null) {
+                            mOpenAnimAdaptor.cleanUpWindowlessSurface(true);
+                        }
+                    }
+                });
+            }
         }
 
         void clearBackAnimateTarget(boolean cancel) {
@@ -1150,6 +1236,7 @@
                 mComposed = false;
                 finishPresentAnimations(cancel);
             }
+            mPrepareCloseTransition = null;
             mWaitTransition = false;
             mStartingSurfaceTargetMatch = false;
             mSwitchType = UNKNOWN;
@@ -1239,6 +1326,9 @@
             // requested one during animating.
             private int mRequestedStartingSurfaceId = INVALID_TASK_ID;
             private SurfaceControl mStartingSurface;
+
+            private Transition mPreparedOpenTransition;
+
             BackWindowAnimationAdaptorWrapper(boolean isOpen, int switchType,
                     @NonNull WindowContainer... targets) {
                 mAdaptors = new BackWindowAnimationAdaptor[targets.length];
@@ -1267,6 +1357,8 @@
                     mCloseTransaction.apply();
                     mCloseTransaction = null;
                 }
+
+                mPreparedOpenTransition = null;
             }
 
             private RemoteAnimationTarget createWrapTarget() {
@@ -1279,8 +1371,7 @@
                     unionBounds.union(mAdaptors[i].mAnimationTarget.localBounds);
                 }
                 final WindowContainer wc = mAdaptors[0].mTarget;
-                final Task task = wc.asActivityRecord() != null
-                        ? wc.asActivityRecord().getTask() : wc.asTask();
+                final Task task = mAdaptors[0].getTopTask();
                 final RemoteAnimationTarget represent = mAdaptors[0].mAnimationTarget;
                 final SurfaceControl leashSurface = new SurfaceControl.Builder()
                         .setName("cross-animation-leash")
@@ -1293,7 +1384,7 @@
                 mCloseTransaction = new SurfaceControl.Transaction();
                 mCloseTransaction.reparent(leashSurface, null);
                 final SurfaceControl.Transaction pt = wc.getPendingTransaction();
-                pt.setLayer(leashSurface, wc.getParent().getLastLayer());
+                pt.setLayer(leashSurface, wc.getLastLayer());
                 for (int i = mAdaptors.length - 1; i >= 0; --i) {
                     BackWindowAnimationAdaptor adaptor = mAdaptors[i];
                     pt.reparent(adaptor.mAnimationTarget.leash, leashSurface);
@@ -1323,15 +1414,20 @@
                 }
                 final WindowContainer mainOpen = mAdaptors[0].mTarget;
                 final int switchType = mAdaptors[0].mSwitchType;
-                final Task openTask = switchType == TASK_SWITCH
-                        ? mainOpen.asTask() : switchType == ACTIVITY_SWITCH
-                        ? mainOpen.asActivityRecord().getTask() : null;
+                final Task openTask = mAdaptors[0].getTopTask();
                 if (openTask == null) {
                     return;
                 }
-                final ActivityRecord mainActivity = switchType == ACTIVITY_SWITCH
-                        ? mainOpen.asActivityRecord()
-                        : openTask.getTopNonFinishingActivity();
+                ActivityRecord mainActivity = null;
+                if (switchType == ACTIVITY_SWITCH) {
+                    mainActivity = mainOpen.asActivityRecord();
+                    if (mainActivity == null && mainOpen.asTaskFragment() != null) {
+                        mainActivity = mainOpen.asTaskFragment().getTopNonFinishingActivity();
+                    }
+                }
+                if (mainActivity == null) {
+                    mainActivity = openTask.getTopNonFinishingActivity();
+                }
                 if (mainActivity == null) {
                     return;
                 }
@@ -1363,6 +1459,8 @@
                                     synchronized (openTask.mWmService.mGlobalLock) {
                                         if (mRequestedStartingSurfaceId != INVALID_TASK_ID) {
                                             mStartingSurface = sc;
+                                            openTask.mWmService.mWindowPlacerLocked
+                                                    .requestTraversal();
                                         } else {
                                             sc.release();
                                         }
@@ -1371,28 +1469,6 @@
                             });
             }
 
-            // When back gesture has triggered and transition target matches navigation target,
-            // reparent the starting surface to the opening target as it's starting window.
-            void reparentWindowlessSurfaceToTarget(SurfaceControl.Transaction reparentTransaction) {
-                if (mRequestedStartingSurfaceId == INVALID_TASK_ID) {
-                    return;
-                }
-                // If open target matches, reparent to open activity or task
-                if (mStartingSurface != null && mStartingSurface.isValid()) {
-                    SurfaceControl.Transaction transaction = reparentTransaction != null
-                            ? reparentTransaction : mAdaptors[0].mTarget.getPendingTransaction();
-                    if (mAdaptors.length != 1) {
-                        // More than one opening window, reparent starting surface to leaf task.
-                        final WindowContainer wc = mAdaptors[0].mTarget;
-                        final Task task = wc.asActivityRecord() != null
-                                ? wc.asActivityRecord().getTask() : wc.asTask();
-                        transaction.reparent(mStartingSurface, task != null
-                                        ? task.getSurfaceControl()
-                                        : mAdaptors[0].mTarget.getSurfaceControl());
-                    }
-                }
-            }
-
             /**
              * Ask shell to clear the starting surface.
              * @param openTransitionMatch if true, shell will play the remove starting window
@@ -1430,6 +1506,22 @@
                 mSwitchType = switchType;
             }
 
+            Task getTopTask() {
+                final Task asTask = mTarget.asTask();
+                if (asTask != null) {
+                    return asTask;
+                }
+                final ActivityRecord ar = mTarget.asActivityRecord();
+                if (ar != null) {
+                    return ar.getTask();
+                }
+                final TaskFragment tf = mTarget.asTaskFragment();
+                if (tf != null) {
+                    return tf.getTask();
+                }
+                return null;
+            }
+
             @Override
             public boolean getShowWallpaper() {
                 return false;
@@ -1619,9 +1711,8 @@
                     needsLaunchBehind = snapshot == null;
                 }
                 if (needsLaunchBehind) {
-                    for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
-                        setLaunchBehind(visibleOpenActivities[i]);
-                    }
+                    openAnimationAdaptor.mPreparedOpenTransition =
+                            setLaunchBehind(visibleOpenActivities);
                 }
                 // Force update mLastSurfaceShowing for opening activity and its task.
                 if (mWindowManagerService.mRoot.mTransitionController.isShellTransitionsEnabled()) {
@@ -1677,13 +1768,22 @@
                                 // animation was canceled
                                 return;
                             }
-                            if (!triggerBack) {
-                                clearBackAnimateTarget(true /* cancel */);
+                            if (Flags.migratePredictiveBackTransition()) {
+                                if (mOpenAnimAdaptor == null
+                                        || mOpenAnimAdaptor.mPreparedOpenTransition == null) {
+                                    // no open nor close transition, this is window animation
+                                    if (!triggerBack) {
+                                        clearBackAnimateTarget(true /* cancel */);
+                                    }
+                                }
                             } else {
-                                mWaitTransition = true;
+                                if (!triggerBack) {
+                                    clearBackAnimateTarget(true /* cancel */);
+                                } else {
+                                    mWaitTransition = true;
+                                }
                             }
                         }
-                        // TODO Add timeout monitor if transition didn't happen
                     }
                 };
             }
@@ -1740,28 +1840,75 @@
         return openActivities;
     }
 
-    private static void setLaunchBehind(@NonNull ActivityRecord activity) {
-        if (!activity.isVisibleRequested()) {
-            // The transition could commit the visibility and in the finishing state, that could
-            // skip commitVisibility call in setVisibility cause the activity won't visible here.
-            // Call it again to make sure the activity could be visible while handling the pending
-            // animation.
-            // Do not performLayout during prepare animation, because it could cause focus window
-            // change. Let that happen after the BackNavigationInfo has returned to shell.
-            activity.commitVisibility(true, false /* performLayout */);
+    private static Transition setLaunchBehind(@NonNull ActivityRecord[] activities) {
+        final boolean migrateBackTransition = Flags.migratePredictiveBackTransition();
+        final ArrayList<ActivityRecord> affects = new ArrayList<>();
+        for (int i = activities.length - 1; i >= 0; --i) {
+            final ActivityRecord activity = activities[i];
+            if (activity.mLaunchTaskBehind || activity.isVisibleRequested()) {
+                continue;
+            }
+            affects.add(activity);
+        }
+        if (affects.isEmpty()) {
+            return null;
+        }
+
+        final TransitionController tc = activities[0].mTransitionController;
+        final Transition prepareOpen = migrateBackTransition && !tc.isCollecting()
+                ? tc.createTransition(TRANSIT_PREPARE_BACK_NAVIGATION) : null;
+
+        for (int i = affects.size() - 1; i >= 0; --i) {
+            final ActivityRecord activity = affects.get(i);
+            if (!migrateBackTransition && !activity.isVisibleRequested()) {
+                // The transition could commit the visibility and in the finishing state, that could
+                // skip commitVisibility call in setVisibility cause the activity won't visible
+                // here.
+                // Call it again to make sure the activity could be visible while handling the
+                // pending animation.
+                // Do not performLayout during prepare animation, because it could cause focus
+                // window change. Let that happen after the BackNavigationInfo has returned to
+                // shell.
+                activity.commitVisibility(true, false /* performLayout */);
+            }
             activity.mTransitionController.mSnapshotController
                     .mActivitySnapshotController.addOnBackPressedActivity(activity);
-        }
-        activity.mLaunchTaskBehind = true;
+            activity.mLaunchTaskBehind = true;
 
-        ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
-                "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
-        activity.mTaskSupervisor.mStoppingActivities.remove(activity);
-        activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
-                true /* notifyClients */);
+            ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
+                    "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
+            activity.mTaskSupervisor.mStoppingActivities.remove(activity);
+
+            if (!migrateBackTransition) {
+                activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
+                        true /* notifyClients */);
+            } else if (activity.shouldBeVisible()) {
+                activity.ensureActivityConfiguration(true /* ignoreVisibility */);
+                activity.makeVisibleIfNeeded(null /* starting */, true /* notifyToClient */);
+            }
+        }
+        boolean needTransition = false;
+        final DisplayContent dc = affects.get(0).getDisplayContent();
+        for (int i = affects.size() - 1; i >= 0; --i) {
+            final ActivityRecord activity = affects.get(i);
+            needTransition |= tc.isCollecting(activity);
+        }
+        if (prepareOpen != null) {
+            if (needTransition) {
+                tc.requestStartTransition(prepareOpen,
+                        null /*startTask */, null /* remoteTransition */,
+                        null /* displayChange */);
+                tc.setReady(dc);
+                return prepareOpen;
+            } else {
+                prepareOpen.abort();
+            }
+        }
+        return null;
     }
 
-    private static void restoreLaunchBehind(@NonNull ActivityRecord activity, boolean cancel) {
+    private static void restoreLaunchBehind(@NonNull ActivityRecord activity, boolean cancel,
+            boolean finishTransition) {
         if (!activity.isAttached()) {
             // The activity was detached from hierarchy.
             return;
@@ -1771,9 +1918,15 @@
                 "Setting Activity.mLauncherTaskBehind to false. Activity=%s",
                 activity);
         if (cancel) {
-            // Restore the launch-behind state
-            // TODO b/347168362 Change status directly during collecting for a transition.
-            activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token);
+            final boolean migrateBackTransition = Flags.migratePredictiveBackTransition();
+            if (migrateBackTransition && finishTransition) {
+                activity.commitVisibility(false /* visible */, false /* performLayout */,
+                        true /* fromTransition */);
+            } else {
+                // Restore the launch-behind state
+                // TODO b/347168362 Change status directly during collecting for a transition.
+                activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token);
+            }
             // Ignore all change
             activity.mTransitionController.mSnapshotController
                     .mActivitySnapshotController.clearOnBackPressedActivities();
@@ -1835,12 +1988,7 @@
                         && ah.mSwitchType != AnimationHandler.ACTIVITY_SWITCH)) {
                     return;
                 }
-                for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) {
-                    final ActivityRecord preDrawActivity = mAnimationHandler.mOpenActivities[i];
-                    if (!preDrawActivity.mLaunchTaskBehind) {
-                        setLaunchBehind(preDrawActivity);
-                    }
-                }
+                setLaunchBehind(mAnimationHandler.mOpenActivities);
             }
         }
     }
@@ -1853,10 +2001,15 @@
             snapshot = task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot(
                     task.mTaskId, task.mUserId, false /* restoreFromDisk */,
                     false /* isLowResolution */);
-        } else if (w.asActivityRecord() != null) {
-            final ActivityRecord ar = w.asActivityRecord();
-            snapshot = ar.mWmService.mSnapshotController.mActivitySnapshotController
-                    .getSnapshot(visibleOpenActivities);
+        } else {
+            ActivityRecord ar = w.asActivityRecord();
+            if (ar == null && w.asTaskFragment() != null) {
+                ar = w.asTaskFragment().getTopNonFinishingActivity();
+            }
+            if (ar != null) {
+                snapshot = ar.mWmService.mSnapshotController.mActivitySnapshotController
+                        .getSnapshot(visibleOpenActivities);
+            }
         }
 
         return isSnapshotCompatible(snapshot, visibleOpenActivities) ? snapshot : null;
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 27e6e09..7135c3b 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -44,7 +44,6 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider;
-import com.android.window.flags.Flags;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -145,9 +144,6 @@
      * @see #DATA_DISPLAY_SETTINGS_FILE_PATH
      */
     void setOverrideSettingsForUser(@UserIdInt int userId) {
-        if (!Flags.perUserDisplayWindowSettings()) {
-            return;
-        }
         final AtomicFile settingsFile = getOverrideSettingsFileForUser(userId);
         setOverrideSettingsStorage(new AtomicFileStorage(settingsFile));
     }
@@ -165,9 +161,6 @@
      */
     void removeStaleDisplaySettingsLocked(@NonNull WindowManagerService wms,
             @NonNull RootWindowContainer root) {
-        if (!Flags.perUserDisplayWindowSettings()) {
-            return;
-        }
         final Set<String> displayIdentifiers = new ArraySet<>();
         final Consumer<DisplayInfo> addDisplayIdentifier =
                 displayInfo -> displayIdentifiers.add(mOverrideSettings.getIdentifier(displayInfo));
@@ -403,12 +396,9 @@
 
     @NonNull
     private static AtomicFile getOverrideSettingsFileForUser(@UserIdInt int userId) {
-        final File directory;
-        if (userId == USER_SYSTEM || !Flags.perUserDisplayWindowSettings()) {
-            directory = Environment.getDataDirectory();
-        } else {
-            directory = Environment.getDataSystemCeDirectory(userId);
-        }
+        final File directory = (userId == USER_SYSTEM)
+                ? Environment.getDataDirectory()
+                : Environment.getDataSystemCeDirectory(userId);
         final File overrideSettingsFile = new File(directory, DATA_DISPLAY_SETTINGS_FILE_PATH);
         return new AtomicFile(overrideSettingsFile, WM_DISPLAY_COMMIT_TAG);
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 291eab1..eb8a637 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -16,37 +16,16 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 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 com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
-import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
-import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString;
 
 import android.annotation.NonNull;
@@ -68,7 +47,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 
@@ -92,8 +70,6 @@
 
     private boolean mLastShouldShowLetterboxUi;
 
-    private boolean mDoubleTapEvent;
-
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
         mAppCompatConfiguration = wmService.mAppCompatConfiguration;
         // Given activityRecord may not be fully constructed since LetterboxUiController
@@ -231,7 +207,8 @@
                     .getTransparentPolicy().isRunning()
                     ? mActivityRecord.getBounds() : w.getFrame();
             mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
-            if (mDoubleTapEvent) {
+            if (mActivityRecord.mAppCompatController
+                    .getAppCompatReachabilityOverrides().isDoubleTapEvent()) {
                 // We need to notify Shell that letterbox position has changed.
                 mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
             }
@@ -240,12 +217,6 @@
         }
     }
 
-    boolean isFromDoubleTap() {
-        final boolean isFromDoubleTap = mDoubleTapEvent;
-        mDoubleTapEvent = false;
-        return isFromDoubleTap;
-    }
-
     SurfaceControl getLetterboxParentSurface() {
         if (mActivityRecord.isInLetterboxAnimation()) {
             return mActivityRecord.getTask().getSurfaceControl();
@@ -272,309 +243,35 @@
                 && mActivityRecord.fillsParent();
     }
 
-    // Check if we are in the given pose and in fullscreen mode.
-    // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
-    // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
-    // actually fullscreen. If display is still in transition e.g. unfolding, don't return true
-    // for HALF_FOLDED state or app will flicker.
-    boolean isDisplayFullScreenAndInPosture(boolean isTabletop) {
-        Task task = mActivityRecord.getTask();
-        return mActivityRecord.mDisplayContent != null && task != null
-                && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(
-                        DeviceStateController.DeviceState.HALF_FOLDED, isTabletop)
-                && !mActivityRecord.mDisplayContent.inTransition()
-                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+    float getHorizontalPositionMultiplier(@NonNull Configuration parentConfiguration) {
+        return mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides()
+                .getHorizontalPositionMultiplier(parentConfiguration);
     }
 
-    // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
-    // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
-    // actually fullscreen.
-    private boolean isDisplayFullScreenAndSeparatingHinge() {
-        Task task = mActivityRecord.getTask();
-        return mActivityRecord.mDisplayContent != null
-                && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
-                && task != null
-                && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
-    }
-
-
-    float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
-        // Don't check resolved configuration because it may not be updated yet during
-        // configuration change.
-        boolean bookModeEnabled = isFullScreenAndBookModeEnabled();
-        return isHorizontalReachabilityEnabled(parentConfiguration)
-                // Using the last global dynamic position to avoid "jumps" when moving
-                // between apps or activities.
-                ? mAppCompatConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
-                : mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
-    }
-
-    private boolean isFullScreenAndBookModeEnabled() {
-        return isDisplayFullScreenAndInPosture(/* isTabletop */ false)
-                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
-    }
-
-    float getVerticalPositionMultiplier(Configuration parentConfiguration) {
-        // Don't check resolved configuration because it may not be updated yet during
-        // configuration change.
-        boolean tabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
-        return isVerticalReachabilityEnabled(parentConfiguration)
-                // Using the last global dynamic position to avoid "jumps" when moving
-                // between apps or activities.
-                ? mAppCompatConfiguration.getVerticalMultiplierForReachability(tabletopMode)
-                : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
+    float getVerticalPositionMultiplier(@NonNull Configuration parentConfiguration) {
+        return mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides()
+                .getVerticalPositionMultiplier(parentConfiguration);
     }
 
     boolean isLetterboxEducationEnabled() {
         return mAppCompatConfiguration.getIsEducationEnabled();
     }
 
-    /**
-     * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
-     */
-    boolean isVerticalThinLetterboxed() {
-        final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx();
-        if (thinHeight < 0) {
-            return false;
-        }
-        final Task task = mActivityRecord.getTask();
-        if (task == null) {
-            return false;
-        }
-        final int padding = Math.abs(
-                task.getBounds().height() - mActivityRecord.getBounds().height()) / 2;
-        return padding <= thinHeight;
-    }
-
-    /**
-     * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
-     */
-    boolean isHorizontalThinLetterboxed() {
-        final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx();
-        if (thinWidth < 0) {
-            return false;
-        }
-        final Task task = mActivityRecord.getTask();
-        if (task == null) {
-            return false;
-        }
-        final int padding = Math.abs(
-                task.getBounds().width() - mActivityRecord.getBounds().width()) / 2;
-        return padding <= thinWidth;
-    }
-
-
-    /**
-     * @return {@value true} if the vertical reachability should be allowed in case of
-     * thin letteboxing
-     */
-    boolean allowVerticalReachabilityForThinLetterbox() {
-        if (!Flags.disableThinLetterboxingPolicy()) {
-            return true;
-        }
-        // When the flag is enabled we allow vertical reachability only if the
-        // app is not thin letterboxed vertically.
-        return !isVerticalThinLetterboxed();
-    }
-
-    /**
-     * @return {@value true} if the vertical reachability should be enabled in case of
-     * thin letteboxing
-     */
-    boolean allowHorizontalReachabilityForThinLetterbox() {
-        if (!Flags.disableThinLetterboxingPolicy()) {
-            return true;
-        }
-        // When the flag is enabled we allow horizontal reachability only if the
-        // app is not thin pillarboxed.
-        return !isHorizontalThinLetterboxed();
-    }
-
-    boolean shouldOverrideMinAspectRatio() {
-        return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio();
-    }
-
-    @AppCompatConfiguration.LetterboxVerticalReachabilityPosition
-    int getLetterboxPositionForVerticalReachability() {
-        final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
-        return mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(
-                isInFullScreenTabletopMode);
-    }
-
-    @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition
-    int getLetterboxPositionForHorizontalReachability() {
-        final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
-        return mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(
-                isInFullScreenBookMode);
-    }
-
     @VisibleForTesting
     void handleHorizontalDoubleTap(int x) {
-        if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) {
-            return;
-        }
-
-        if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) {
-            // Only react to clicks at the sides of the letterboxed app window.
-            return;
-        }
-
-        boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge()
-                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
-        int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
-                .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
-        if (mLetterbox.getInnerFrame().left > x) {
-            // Moving to the next stop on the left side of the app window: right > center > left.
-            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
-                    isInFullScreenBookMode);
-            int changeToLog =
-                    letterboxPositionForHorizontalReachability
-                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
-                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
-                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
-            logLetterboxPositionChange(changeToLog);
-            mDoubleTapEvent = true;
-        } else if (mLetterbox.getInnerFrame().right < x) {
-            // Moving to the next stop on the right side of the app window: left > center > right.
-            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
-                    isInFullScreenBookMode);
-            int changeToLog =
-                    letterboxPositionForHorizontalReachability
-                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
-                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
-                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
-            logLetterboxPositionChange(changeToLog);
-            mDoubleTapEvent = true;
-        }
-        // TODO(197549949): Add animation for transition.
-        mActivityRecord.recomputeConfiguration();
+        mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+                .handleHorizontalDoubleTap(x, mLetterbox::getInnerFrame);
     }
 
     @VisibleForTesting
     void handleVerticalDoubleTap(int y) {
-        if (!isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) {
-            return;
-        }
-
-        if (mLetterbox.getInnerFrame().top <= y && mLetterbox.getInnerFrame().bottom >= y) {
-            // Only react to clicks at the top and bottom of the letterboxed app window.
-            return;
-        }
-        boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
-        int letterboxPositionForVerticalReachability = mAppCompatConfiguration
-                .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
-        if (mLetterbox.getInnerFrame().top > y) {
-            // Moving to the next stop on the top side of the app window: bottom > center > top.
-            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextTopStop(
-                    isInFullScreenTabletopMode);
-            int changeToLog =
-                    letterboxPositionForVerticalReachability
-                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
-                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
-                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
-            logLetterboxPositionChange(changeToLog);
-            mDoubleTapEvent = true;
-        } else if (mLetterbox.getInnerFrame().bottom < y) {
-            // Moving to the next stop on the bottom side of the app window: top > center > bottom.
-            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
-                    isInFullScreenTabletopMode);
-            int changeToLog =
-                    letterboxPositionForVerticalReachability
-                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
-                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
-                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
-            logLetterboxPositionChange(changeToLog);
-            mDoubleTapEvent = true;
-        }
-        // TODO(197549949): Add animation for transition.
-        mActivityRecord.recomputeConfiguration();
-    }
-
-    /**
-     * Whether horizontal reachability is enabled for an activity in the current configuration.
-     *
-     * <p>Conditions that needs to be met:
-     * <ul>
-     *   <li>Windowing mode is fullscreen.
-     *   <li>Horizontal Reachability is enabled.
-     *   <li>First top opaque activity fills parent vertically, but not horizontally.
-     * </ul>
-     */
-    private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
-        if (!allowHorizontalReachabilityForThinLetterbox()) {
-            return false;
-        }
-        final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
-        final Rect parentAppBounds = parentAppBoundsOverride != null
-                ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
-        // Use screen resolved bounds which uses resolved bounds or size compat bounds
-        // as activity bounds can sometimes be empty
-        final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController
-                .getTransparentPolicy().getFirstOpaqueActivity()
-                .map(ActivityRecord::getScreenResolvedBounds)
-                .orElse(mActivityRecord.getScreenResolvedBounds());
-        return mAppCompatConfiguration.getIsHorizontalReachabilityEnabled()
-                && parentConfiguration.windowConfiguration.getWindowingMode()
-                        == WINDOWING_MODE_FULLSCREEN
-                // Check whether the activity fills the parent vertically.
-                && parentAppBounds.height() <= opaqueActivityBounds.height()
-                && parentAppBounds.width() > opaqueActivityBounds.width();
-    }
-
-    @VisibleForTesting
-    boolean isHorizontalReachabilityEnabled() {
-        return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
-    }
-
-    boolean isLetterboxDoubleTapEducationEnabled() {
-        return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled();
-    }
-
-    // TODO(b/346264992): Remove after AppCompatController refactoring
-    private AppCompatOverrides getAppCompatOverrides() {
-        return mActivityRecord.mAppCompatController.getAppCompatOverrides();
-    }
-
-    /**
-     * Whether vertical reachability is enabled for an activity in the current configuration.
-     *
-     * <p>Conditions that needs to be met:
-     * <ul>
-     *   <li>Windowing mode is fullscreen.
-     *   <li>Vertical Reachability is enabled.
-     *   <li>First top opaque activity fills parent horizontally but not vertically.
-     * </ul>
-     */
-    private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
-        if (!allowVerticalReachabilityForThinLetterbox()) {
-            return false;
-        }
-        final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
-        final Rect parentAppBounds = parentAppBoundsOverride != null
-                ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
-        // Use screen resolved bounds which uses resolved bounds or size compat bounds
-        // as activity bounds can sometimes be empty.
-        final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController
-                .getTransparentPolicy().getFirstOpaqueActivity()
-                .map(ActivityRecord::getScreenResolvedBounds)
-                .orElse(mActivityRecord.getScreenResolvedBounds());
-        return mAppCompatConfiguration.getIsVerticalReachabilityEnabled()
-                && parentConfiguration.windowConfiguration.getWindowingMode()
-                        == WINDOWING_MODE_FULLSCREEN
-                // Check whether the activity fills the parent horizontally.
-                && parentAppBounds.width() <= opaqueActivityBounds.width()
-                && parentAppBounds.height() > opaqueActivityBounds.height();
-    }
-
-    @VisibleForTesting
-    boolean isVerticalReachabilityEnabled() {
-        return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
+        mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+                .handleVerticalDoubleTap(y, mLetterbox::getInnerFrame);
     }
 
     @VisibleForTesting
     boolean shouldShowLetterboxUi(WindowState mainWindow) {
-        if (getAppCompatOverrides().getAppCompatOrientationOverrides()
+        if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
                 .getIsRelaunchingAfterRequestedOrientationChanged()) {
             return mLastShouldShowLetterboxUi;
         }
@@ -818,8 +515,10 @@
         if (!shouldShowLetterboxUi) {
             return;
         }
-        pw.println(prefix + "  isVerticalThinLetterboxed=" + isVerticalThinLetterboxed());
-        pw.println(prefix + "  isHorizontalThinLetterboxed=" + isHorizontalThinLetterboxed());
+        pw.println(prefix + "  isVerticalThinLetterboxed=" + mActivityRecord.mAppCompatController
+                .getAppCompatReachabilityOverrides().isVerticalThinLetterboxed());
+        pw.println(prefix + "  isHorizontalThinLetterboxed=" + mActivityRecord.mAppCompatController
+                .getAppCompatReachabilityOverrides().isHorizontalThinLetterboxed());
         pw.println(prefix + "  letterboxBackgroundColor=" + Integer.toHexString(
                 getLetterboxBackgroundColor().toArgb()));
         pw.println(prefix + "  letterboxBackgroundType="
@@ -836,10 +535,12 @@
             pw.println(prefix + "  letterboxBackgroundWallpaperBlurRadius="
                     + getLetterboxWallpaperBlurRadiusPx());
         }
-
+        final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord
+                .mAppCompatController.getAppCompatReachabilityOverrides();
         pw.println(prefix + "  isHorizontalReachabilityEnabled="
-                + isHorizontalReachabilityEnabled());
-        pw.println(prefix + "  isVerticalReachabilityEnabled=" + isVerticalReachabilityEnabled());
+                + reachabilityOverrides.isHorizontalReachabilityEnabled());
+        pw.println(prefix + "  isVerticalReachabilityEnabled="
+                + reachabilityOverrides.isVerticalReachabilityEnabled());
         pw.println(prefix + "  letterboxHorizontalPositionMultiplier="
                 + getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
         pw.println(prefix + "  letterboxVerticalPositionMultiplier="
@@ -883,64 +584,6 @@
         return "UNKNOWN_REASON";
     }
 
-    private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
-            @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position) {
-        switch (position) {
-            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
-            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
-            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
-            default:
-                throw new AssertionError(
-                        "Unexpected letterbox horizontal reachability position type: "
-                                + position);
-        }
-    }
-
-    private int letterboxVerticalReachabilityPositionToLetterboxPosition(
-            @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int position) {
-        switch (position) {
-            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
-            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
-            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
-                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
-            default:
-                throw new AssertionError(
-                        "Unexpected letterbox vertical reachability position type: "
-                                + position);
-        }
-    }
-
-    int getLetterboxPositionForLogging() {
-        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
-        if (isHorizontalReachabilityEnabled()) {
-            int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
-                    .getLetterboxPositionForHorizontalReachability(
-                            isDisplayFullScreenAndInPosture(/* isTabletop */ false));
-            positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
-                    letterboxPositionForHorizontalReachability);
-        } else if (isVerticalReachabilityEnabled()) {
-            int letterboxPositionForVerticalReachability = mAppCompatConfiguration
-                    .getLetterboxPositionForVerticalReachability(
-                            isDisplayFullScreenAndInPosture(/* isTabletop */ true));
-            positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
-                    letterboxPositionForVerticalReachability);
-        }
-        return positionToLog;
-    }
-
-    /**
-     * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
-     */
-    private void logLetterboxPositionChange(int letterboxPositionChange) {
-        mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
-                .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
-    }
-
     @Nullable
     LetterboxDetails getLetterboxDetails() {
         final WindowState w = mActivityRecord.findMainWindow();
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 7ada4c7..d3df5fd 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3389,7 +3389,6 @@
         info.isTopActivityTransparent = top != null && !top.fillsParent();
         info.isTopActivityStyleFloating = top != null && top.isStyleFloating();
         info.lastNonFullscreenBounds = topTask.mLastNonFullscreenBounds;
-
         AppCompatUtils.fillAppCompatTaskInfo(this, info, top);
     }
 
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 996b439..5698750 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1764,7 +1764,7 @@
 
         // Check whether the participants were animated from back navigation.
         mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets,
-                transaction);
+                transaction, mFinishTransaction);
         final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
         info.setDebugId(mSyncId);
         mController.assignTrack(this, info);
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 51d5bc0..092a751 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -284,30 +284,28 @@
 
     private int runDisplayDensity(PrintWriter pw) throws RemoteException {
         String densityStr = getNextArg();
-        String option = getNextOption();
         String arg = getNextArg();
         int density;
         int displayId = Display.DEFAULT_DISPLAY;
-        if ("-d".equals(option) && arg != null) {
+        if ("-d".equals(densityStr) && arg != null) {
             try {
                 displayId = Integer.parseInt(arg);
             } catch (NumberFormatException e) {
                 getErrPrintWriter().println("Error: bad number " + e);
             }
-        } else if ("-u".equals(option) && arg != null) {
+            densityStr = getNextArg();
+        } else if ("-u".equals(densityStr) && arg != null) {
             displayId = mInterface.getDisplayIdByUniqueId(arg);
             if (displayId == Display.INVALID_DISPLAY) {
                 getErrPrintWriter().println("Error: the uniqueId is invalid ");
                 return -1;
             }
+            densityStr = getNextArg();
         }
 
         if (densityStr == null) {
             printInitialDisplayDensity(pw, displayId);
             return 0;
-        } else if ("-d".equals(densityStr)) {
-            printInitialDisplayDensity(pw, displayId);
-            return 0;
         } else if ("reset".equals(densityStr)) {
             density = -1;
         } else {
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 9fa1a53..f1e94de 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -79,6 +79,7 @@
         "com_android_server_wm_TaskFpsCallbackController.cpp",
         "onload.cpp",
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         ":lib_anrTimer_native",
@@ -241,6 +242,13 @@
 }
 
 filegroup {
+    name: "lib_freezer_native",
+    srcs: [
+        "com_android_server_am_Freezer.cpp",
+    ],
+}
+
+filegroup {
     name: "lib_gameManagerService_native",
     srcs: [
         "com_android_server_app_GameManagerService.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 1667e27..b622751 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -27,7 +27,8 @@
 per-file com_android_server_security_* = file:/core/java/android/security/OWNERS
 per-file com_android_server_tv_* = file:/media/java/android/media/tv/OWNERS
 per-file com_android_server_vibrator_* = file:/services/core/java/com/android/server/vibrator/OWNERS
-per-file com_android_server_am_CachedAppOptimizer.cpp = timmurray@google.com, edgararriaga@google.com, dualli@google.com, carmenjackson@google.com, philipcuadra@google.com
+per-file com_android_server_am_CachedAppOptimizer.cpp = file:/PERFORMANCE_OWNERS
+per-file com_android_server_am_Freezer.cpp = file:/PERFORMANCE_OWNERS
 per-file com_android_server_companion_virtual_InputController.cpp = file:/services/companion/java/com/android/server/companion/virtual/OWNERS
 
 # Memory
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 95e7b19..a91fd08 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -24,7 +24,6 @@
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
 #include <android_runtime/AndroidRuntime.h>
-#include <binder/IPCThreadState.h>
 #include <cutils/compiler.h>
 #include <dirent.h>
 #include <jni.h>
@@ -34,7 +33,6 @@
 #include <meminfo/procmeminfo.h>
 #include <meminfo/sysmeminfo.h>
 #include <nativehelper/JNIHelp.h>
-#include <processgroup/processgroup.h>
 #include <stddef.h>
 #include <stdio.h>
 #include <sys/mman.h>
@@ -63,10 +61,6 @@
 using VmaToAdviseFunc = std::function<int(const Vma&)>;
 using android::base::unique_fd;
 
-#define SYNC_RECEIVED_WHILE_FROZEN (1)
-#define ASYNC_RECEIVED_WHILE_FROZEN (2)
-#define TXNS_PENDING_WHILE_FROZEN (4)
-
 #define MAX_RW_COUNT (INT_MAX & kPageMask)
 
 // Defines the maximum amount of VMAs we can send per process_madvise syscall.
@@ -527,58 +521,6 @@
     compactProcessOrFallback(pid, compactionFlags);
 }
 
-static jint com_android_server_am_CachedAppOptimizer_freezeBinder(JNIEnv* env, jobject clazz,
-                                                                  jint pid, jboolean freeze,
-                                                                  jint timeout_ms) {
-    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
-    if (retVal != 0 && retVal != -EAGAIN) {
-        jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
-    }
-
-    return retVal;
-}
-
-static jint com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo(JNIEnv *env,
-        jobject clazz, jint pid) {
-    uint32_t syncReceived = 0, asyncReceived = 0;
-
-    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
-
-    if (error < 0) {
-        jniThrowException(env, "java/lang/RuntimeException", strerror(error));
-    }
-
-    jint retVal = 0;
-
-    // bit 0 of sync_recv goes to bit 0 of retVal
-    retVal |= syncReceived & SYNC_RECEIVED_WHILE_FROZEN;
-    // bit 0 of async_recv goes to bit 1 of retVal
-    retVal |= (asyncReceived << 1) & ASYNC_RECEIVED_WHILE_FROZEN;
-    // bit 1 of sync_recv goes to bit 2 of retVal
-    retVal |= (syncReceived << 1) & TXNS_PENDING_WHILE_FROZEN;
-
-    return retVal;
-}
-
-static jstring com_android_server_am_CachedAppOptimizer_getFreezerCheckPath(JNIEnv* env,
-                                                                            jobject clazz) {
-    std::string path;
-
-    if (!getAttributePathForTask("FreezerState", getpid(), &path)) {
-        path = "";
-    }
-
-    return env->NewStringUTF(path.c_str());
-}
-
-static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) {
-    uid_t uid = getuid();
-    pid_t pid = getpid();
-
-    return isProfileValidForProcess("Frozen", uid, pid) &&
-            isProfileValidForProcess("Unfrozen", uid, pid);
-}
-
 static const JNINativeMethod sMethods[] = {
         /* name, signature, funcPtr */
         {"cancelCompaction", "()V",
@@ -592,13 +534,7 @@
          (void*)com_android_server_am_CachedAppOptimizer_getMemoryFreedCompaction},
         {"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
         {"compactProcess", "(II)V", (void*)com_android_server_am_CachedAppOptimizer_compactProcess},
-        {"freezeBinder", "(IZI)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
-        {"getBinderFreezeInfo", "(I)I",
-         (void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo},
-        {"getFreezerCheckPath", "()Ljava/lang/String;",
-         (void*)com_android_server_am_CachedAppOptimizer_getFreezerCheckPath},
-        {"isFreezerProfileValid", "()Z",
-         (void*)com_android_server_am_CachedAppOptimizer_isFreezerProfileValid}};
+};
 
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env)
 {
diff --git a/services/core/jni/com_android_server_am_Freezer.cpp b/services/core/jni/com_android_server_am_Freezer.cpp
new file mode 100644
index 0000000..8148728
--- /dev/null
+++ b/services/core/jni/com_android_server_am_Freezer.cpp
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "Freezer"
+//#define LOG_NDEBUG 0
+#define ATRACE_TAG ATRACE_TAG_ACTIVITY_MANAGER
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <android-base/logging.h>
+#include <android-base/unique_fd.h>
+#include <binder/IPCThreadState.h>
+#include <nativehelper/JNIHelp.h>
+#include <processgroup/processgroup.h>
+
+namespace android {
+namespace {
+
+// Binder status bit flags.
+static const int SYNC_RECEIVED_WHILE_FROZEN = 1;
+static const int ASYNC_RECEIVED_WHILE_FROZEN = 2;
+static const int TXNS_PENDING_WHILE_FROZEN = 4;
+
+jint freezeBinder(JNIEnv* env, jobject, jint pid, jboolean freeze, jint timeout_ms) {
+    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
+    if (retVal != 0 && retVal != -EAGAIN) {
+        jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
+    }
+
+    return retVal;
+}
+
+jint getBinderFreezeInfo(JNIEnv *env, jobject, jint pid) {
+    uint32_t syncReceived = 0, asyncReceived = 0;
+
+    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
+
+    if (error < 0) {
+        jniThrowException(env, "java/lang/RuntimeException", strerror(error));
+    }
+
+    jint retVal = 0;
+
+    // bit 0 of sync_recv goes to bit 0 of retVal
+    retVal |= syncReceived & SYNC_RECEIVED_WHILE_FROZEN;
+    // bit 0 of async_recv goes to bit 1 of retVal
+    retVal |= (asyncReceived << 1) & ASYNC_RECEIVED_WHILE_FROZEN;
+    // bit 1 of sync_recv goes to bit 2 of retVal
+    retVal |= (syncReceived << 1) & TXNS_PENDING_WHILE_FROZEN;
+
+    return retVal;
+}
+
+bool isFreezerSupported(JNIEnv *env, jclass) {
+    std::string path;
+    if (!getAttributePathForTask("FreezerState", getpid(), &path)) {
+        ALOGI("No attribute for FreezerState");
+        return false;
+    }
+    base::unique_fd fid(open(path.c_str(), O_RDONLY));
+    if (fid < 0) {
+        ALOGI("Cannot open freezer path \"%s\": %s", path.c_str(), strerror(errno));
+        return false;
+    }
+
+    char state;
+    if (::read(fid, &state, 1) != 1) {
+        ALOGI("Failed to read freezer state: %s", strerror(errno));
+        return false;
+    }
+    if (state != '1' && state != '0') {
+        ALOGE("Unexpected value in cgroup.freeze: %d", state);
+        return false;
+    }
+
+    uid_t uid = getuid();
+    pid_t pid = getpid();
+
+    uint32_t syncReceived = 0, asyncReceived = 0;
+    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
+    if (error < 0) {
+        ALOGE("Unable to read freezer info: %s", strerror(errno));
+        return false;
+    }
+
+    if (!isProfileValidForProcess("Frozen", uid, pid)
+            || !isProfileValidForProcess("Unfrozen", uid, pid)) {
+        ALOGE("Missing freezer profiles");
+        return false;
+    }
+
+    return true;
+}
+
+static const JNINativeMethod sMethods[] = {
+    {"nativeIsFreezerSupported",    "()Z",       (void*) isFreezerSupported },
+    {"nativeFreezeBinder",          "(IZI)I",    (void*) freezeBinder },
+    {"nativeGetBinderFreezeInfo",   "(I)I",      (void*) getBinderFreezeInfo },
+};
+
+} // end of anonymous namespace
+
+int register_android_server_am_Freezer(JNIEnv* env)
+{
+    char const *className = "com/android/server/am/Freezer";
+    return jniRegisterNativeMethods(env, className, sMethods, NELEM(sMethods));
+}
+
+} // end of namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 314ff9d..3c55d18 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -54,6 +54,7 @@
 int register_android_hardware_display_DisplayViewport(JNIEnv* env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
+int register_android_server_am_Freezer(JNIEnv* env);
 int register_android_server_am_LowMemDetector(JNIEnv* env);
 int register_android_server_utils_AnrTimer(JNIEnv *env);
 int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env);
@@ -118,6 +119,7 @@
     register_android_hardware_display_DisplayViewport(env);
     register_android_server_am_OomConnection(env);
     register_android_server_am_CachedAppOptimizer(env);
+    register_android_server_am_Freezer(env);
     register_android_server_am_LowMemDetector(env);
     register_android_server_utils_AnrTimer(env);
     register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f7aeb89..9ed645b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -13634,7 +13634,28 @@
             setBackwardCompatibleUserRestriction(
                     caller, admin, key, enabledFromThisOwner, parent);
         }
-        logUserRestrictionCall(key, enabledFromThisOwner, parent, caller);
+        logUserRestrictionCall(key, enabledFromThisOwner, parent, caller, affectedUserId);
+    }
+
+    @Override
+    public void setUserRestrictionForUser(
+            @NonNull String systemEntity, String key, boolean enabled, @UserIdInt int targetUser) {
+        Objects.requireNonNull(systemEntity);
+
+        CallerIdentity caller = getCallerIdentity();
+        if (caller.getUid() != Process.SYSTEM_UID) {
+            throw new SecurityException("Only system services can call setUserRestrictionForUser"
+                    + " on a target user: " + targetUser);
+        }
+        if (VERBOSE_LOG) {
+            Slogf.v(LOG_TAG, "Creating SystemEnforcingAdmin %s for calling package %s",
+                    systemEntity, caller.getPackageName());
+        }
+        EnforcingAdmin admin = EnforcingAdmin.createSystemEnforcingAdmin(systemEntity);
+
+        setLocalUserRestrictionInternal(admin, key, enabled, targetUser);
+
+        logUserRestrictionCall(key, enabled, /* parent= */ false, caller, targetUser);
     }
 
     private void checkAdminCanSetRestriction(CallerIdentity caller, boolean parent, String key) {
@@ -13755,7 +13776,8 @@
 
         setGlobalUserRestrictionInternal(admin, key, /* enabled= */ true);
 
-        logUserRestrictionCall(key, /* enabled= */ true, /* parent= */ false, caller);
+        logUserRestrictionCall(key, /* enabled= */ true, /* parent= */ false, caller,
+                UserHandle.USER_ALL);
     }
     private void setLocalUserRestrictionInternal(
             EnforcingAdmin admin, String key, boolean enabled, int userId) {
@@ -13791,7 +13813,7 @@
     }
 
     private void logUserRestrictionCall(
-            String key, boolean enabled, boolean parent, CallerIdentity caller) {
+            String key, boolean enabled, boolean parent, CallerIdentity caller, int targetUserId) {
         final int eventId = enabled
                 ? DevicePolicyEnums.ADD_USER_RESTRICTION
                 : DevicePolicyEnums.REMOVE_USER_RESTRICTION;
@@ -13807,8 +13829,9 @@
             SecurityLog.writeEvent(eventTag, caller.getPackageName(), caller.getUserId(), key);
         }
 
-        Slogf.i(LOG_TAG, "Changing user restriction %s to: %b caller: %s",
-                key, enabled, caller.toString());
+        Slogf.i(LOG_TAG, "Changing user restriction %s on %s to: %b caller: %s",
+                key, (targetUserId == UserHandle.USER_ALL ? "all users" : ("user " + targetUserId)),
+                enabled, caller.toString());
     }
 
     @Override
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
index 02590f9..e65e513 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
@@ -21,7 +21,6 @@
 import android.app.admin.Authority;
 import android.app.admin.DeviceAdminAuthority;
 import android.app.admin.DpcAuthority;
-import android.app.admin.PackagePermissionPolicyKey;
 import android.app.admin.RoleAuthority;
 import android.app.admin.UnknownAuthority;
 import android.content.ComponentName;
@@ -57,23 +56,29 @@
     static final String TAG = "EnforcingAdmin";
 
     static final String ROLE_AUTHORITY_PREFIX = "role:";
+    static final String SYSTEM_AUTHORITY_PREFIX = "system:";
     static final String DPC_AUTHORITY = "enterprise";
     static final String DEVICE_ADMIN_AUTHORITY = "device_admin";
     static final String DEFAULT_AUTHORITY = "default";
 
     private static final String ATTR_PACKAGE_NAME = "package-name";
+    private static final String ATTR_SYSTEM_ENTITY = "system-entity";
     private static final String ATTR_CLASS_NAME = "class-name";
     private static final String ATTR_AUTHORITIES = "authorities";
     private static final String ATTR_AUTHORITIES_SEPARATOR = ";";
     private static final String ATTR_USER_ID = "user-id";
     private static final String ATTR_IS_ROLE = "is-role";
+    private static final String ATTR_IS_SYSTEM = "is-system";
 
     private final String mPackageName;
+    // Name of the system entity. Only used when mIsSystemAuthority is true.
+    private final String mSystemEntity;
     // This is needed for DPCs and active admins
     private final ComponentName mComponentName;
     private Set<String> mAuthorities;
     private final int mUserId;
     private final boolean mIsRoleAuthority;
+    private final boolean mIsSystemAuthority;
     private final ActiveAdmin mActiveAdmin;
 
     static EnforcingAdmin createEnforcingAdmin(@NonNull String packageName, int userId,
@@ -106,6 +111,11 @@
                 userId, activeAdmin);
     }
 
+    static EnforcingAdmin createSystemEnforcingAdmin(@NonNull String systemEntity) {
+        Objects.requireNonNull(systemEntity);
+        return new EnforcingAdmin(systemEntity);
+    }
+
     static EnforcingAdmin createEnforcingAdmin(android.app.admin.EnforcingAdmin admin) {
         Objects.requireNonNull(admin);
         Authority authority = admin.getAuthority();
@@ -127,6 +137,7 @@
                     /* activeAdmin = */ null,
                     /* isRoleAuthority = */ true);
         }
+        // TODO(b/324899199): Consider supporting android.app.admin.SystemAuthority.
         return new EnforcingAdmin(admin.getPackageName(), admin.getComponentName(),
                 Set.of(), admin.getUserHandle().getIdentifier(),
                 /* activeAdmin = */ null);
@@ -159,9 +170,11 @@
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(authorities);
 
-        // Role authorities should not be using this constructor
+        // Role/System authorities should not be using this constructor
         mIsRoleAuthority = false;
+        mIsSystemAuthority = false;
         mPackageName = packageName;
+        mSystemEntity = null;
         mComponentName = componentName;
         mAuthorities = new HashSet<>(authorities);
         mUserId = userId;
@@ -173,7 +186,9 @@
 
         // Only role authorities use this constructor.
         mIsRoleAuthority = true;
+        mIsSystemAuthority = false;
         mPackageName = packageName;
+        mSystemEntity = null;
         mUserId = userId;
         mComponentName = null;
         // authorities will be loaded when needed
@@ -181,6 +196,21 @@
         mActiveAdmin = activeAdmin;
     }
 
+    /** Constructor for System authorities. */
+    private EnforcingAdmin(@NonNull String systemEntity) {
+        Objects.requireNonNull(systemEntity);
+
+        // Only system authorities use this constructor.
+        mIsSystemAuthority = true;
+        mIsRoleAuthority = false;
+        mPackageName = null;
+        mSystemEntity = systemEntity;
+        mUserId = UserHandle.USER_SYSTEM;
+        mComponentName = null;
+        mAuthorities = getSystemAuthority(systemEntity);
+        mActiveAdmin = null;
+    }
+
     private EnforcingAdmin(
             String packageName, @Nullable ComponentName componentName, Set<String> authorities,
             int userId, @Nullable ActiveAdmin activeAdmin, boolean isRoleAuthority) {
@@ -188,7 +218,9 @@
         Objects.requireNonNull(authorities);
 
         mIsRoleAuthority = isRoleAuthority;
+        mIsSystemAuthority = false;
         mPackageName = packageName;
+        mSystemEntity = null;
         mComponentName = componentName;
         mAuthorities = new HashSet<>(authorities);
         mUserId = userId;
@@ -204,6 +236,18 @@
         return authorities.isEmpty() ? Set.of(DEFAULT_AUTHORITY) : authorities;
     }
 
+    /**
+     * Returns a set of authorities for system authority.
+     *
+     * <p>Note that a system authority enforcing admin has only one authority that has the package
+     * name of the calling system service. Therefore, the returned set always contains one element.
+     */
+    private static Set<String> getSystemAuthority(String systemEntity) {
+        Set<String> authorities = new HashSet<>();
+        authorities.add(SYSTEM_AUTHORITY_PREFIX + systemEntity);
+        return authorities;
+    }
+
     // TODO(b/259042794): move this logic to RoleManagerLocal
     private static Set<String> getRoles(String packageName, int userId) {
         RoleManagerLocal roleManagerLocal = LocalManagerRegistry.getManager(
@@ -264,6 +308,7 @@
         } else if (mAuthorities.contains(DEVICE_ADMIN_AUTHORITY)) {
             authority = DeviceAdminAuthority.DEVICE_ADMIN_AUTHORITY;
         } else {
+            // For now, System Authority returns UNKNOWN_AUTHORITY.
             authority = UnknownAuthority.UNKNOWN_AUTHORITY;
         }
         return new android.app.admin.EnforcingAdmin(
@@ -291,8 +336,10 @@
         if (o == null || getClass() != o.getClass()) return false;
         EnforcingAdmin other = (EnforcingAdmin) o;
         return Objects.equals(mPackageName, other.mPackageName)
+                && Objects.equals(mSystemEntity, other.mSystemEntity)
                 && Objects.equals(mComponentName, other.mComponentName)
                 && Objects.equals(mIsRoleAuthority, other.mIsRoleAuthority)
+                && (mIsSystemAuthority == other.mIsSystemAuthority)
                 && hasMatchingAuthorities(this, other);
     }
 
@@ -307,6 +354,8 @@
     public int hashCode() {
         if (mIsRoleAuthority) {
             return Objects.hash(mPackageName, mUserId);
+        } else if (mIsSystemAuthority) {
+            return Objects.hash(mSystemEntity);
         } else {
             return Objects.hash(
                     mComponentName == null ? mPackageName : mComponentName,
@@ -318,8 +367,12 @@
     void saveToXml(TypedXmlSerializer serializer) throws IOException {
         serializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName);
         serializer.attributeBoolean(/* namespace= */ null, ATTR_IS_ROLE, mIsRoleAuthority);
+        serializer.attributeBoolean(/* namespace= */ null, ATTR_IS_SYSTEM, mIsSystemAuthority);
         serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, mUserId);
-        if (!mIsRoleAuthority) {
+        if (mIsSystemAuthority) {
+            serializer.attribute(/* namespace= */ null, ATTR_SYSTEM_ENTITY, mSystemEntity);
+        }
+        if (!mIsRoleAuthority && !mIsSystemAuthority) {
             if (mComponentName != null) {
                 serializer.attribute(
                         /* namespace= */ null, ATTR_CLASS_NAME, mComponentName.getClassName());
@@ -336,7 +389,10 @@
     static EnforcingAdmin readFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException {
         String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME);
+        String systemEntity = parser.getAttributeValue(/* namespace= */ null, ATTR_SYSTEM_ENTITY);
         boolean isRoleAuthority = parser.getAttributeBoolean(/* namespace= */ null, ATTR_IS_ROLE);
+        boolean isSystemAuthority = parser.getAttributeBoolean(
+                /* namespace= */ null, ATTR_IS_SYSTEM, /* defaultValue= */ false);
         String authoritiesStr = parser.getAttributeValue(/* namespace= */ null, ATTR_AUTHORITIES);
         int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
 
@@ -348,6 +404,13 @@
             }
             // TODO(b/281697976): load active admin
             return new EnforcingAdmin(packageName, userId, null);
+        } else if (isSystemAuthority) {
+            if (systemEntity == null) {
+                Slogf.wtf(TAG, "Error parsing EnforcingAdmin with SystemAuthority, "
+                        + "systemEntity is null.");
+                return null;
+            }
+            return new EnforcingAdmin(systemEntity);
         } else {
             if (packageName == null || authoritiesStr == null) {
                 Slogf.wtf(TAG, "Error parsing EnforcingAdmin, packageName is "
@@ -381,6 +444,10 @@
         sb.append(mUserId);
         sb.append(", mIsRoleAuthority= ");
         sb.append(mIsRoleAuthority);
+        sb.append(", mIsSystemAuthority= ");
+        sb.append(mIsSystemAuthority);
+        sb.append(", mSystemEntity = ");
+        sb.append(mSystemEntity);
         sb.append(" }");
         return sb.toString();
     }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 1e3b7e9..e552621 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -79,10 +79,8 @@
         // from system.
         synchronized (ImfLock.class) {
             mBindingController =
-                    new InputMethodBindingController(
-                            mInputMethodManagerService.getCurrentImeUserIdLocked(),
-                            mInputMethodManagerService, mImeConnectionBindFlags,
-                            mCountDownLatch);
+                    new InputMethodBindingController(mUserId, mInputMethodManagerService,
+                            mImeConnectionBindFlags, mCountDownLatch);
         }
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
index 2ea2e22..a86d61bb 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
@@ -122,8 +122,8 @@
 
     private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList,
             List<String> enabledComponents) {
-        final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices(
-                AdditionalSubtypeMap.EMPTY_MAP, enabledComponents, mContext, resolveInfoList);
+        final var methodMap = InputMethodManagerService.filterInputMethodServices(
+                enabledComponents, mContext, resolveInfoList);
         return methodMap.values();
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 461697c..9a25104 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -269,15 +269,21 @@
         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
         lifecycle.onStart();
 
+        final var userData = mInputMethodManagerService.getUserData(mUserId);
+
         // Certain tests rely on TEST_IME_ID that is installed with AndroidTest.xml.
         // TODO(b/352615651): Consider just synthesizing test InputMethodInfo then injecting it.
         AdditionalSubtypeMapRepository.initializeIfNecessary(mUserId);
-        final var settings = InputMethodManagerService.queryInputMethodServicesInternal(mContext,
-                mUserId, AdditionalSubtypeMapRepository.get(mUserId), DirectBootAwareness.AUTO);
+        final var rawMethodMap = InputMethodManagerService.queryRawInputMethodServiceMap(mContext,
+                mUserId);
+        userData.mRawInputMethodMap.set(rawMethodMap);
+        final var settings = InputMethodSettings.create(rawMethodMap.toInputMethodMap(
+                AdditionalSubtypeMap.EMPTY_MAP, DirectBootAwareness.AUTO, true /* userUnlocked */),
+                mUserId);
         InputMethodSettingsRepository.put(mUserId, settings);
 
         // Emulate that the user initialization is done.
-        mInputMethodManagerService.getUserData(mUserId).mBackgroundLoadLatch.countDown();
+        userData.mBackgroundLoadLatch.countDown();
 
         // After this boot phase, services can broadcast Intents.
         lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index b5278a5..6394b27 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1613,16 +1613,21 @@
         advanceTime(1); // Run updatePowerState
 
         reset(mHolder.wakelockController);
+        when(mHolder.wakelockController
+                .acquireWakelock(WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE))
+                .thenReturn(true);
         mHolder.dpc.overrideDozeScreenState(
                 supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
-        advanceTime(1); // Run updatePowerState
 
         // Should get a wakelock to notify powermanager
-        verify(mHolder.wakelockController, atLeastOnce()).acquireWakelock(
-                eq(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS));
+        verify(mHolder.wakelockController).acquireWakelock(
+                eq(WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE));
 
+        advanceTime(1); // Run updatePowerState
         verify(mHolder.displayPowerState)
                 .setScreenState(supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
+        verify(mHolder.wakelockController).releaseWakelock(
+                eq(WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE));
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java
index c23d4b1..019b70e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java
@@ -64,6 +64,8 @@
                 "[" + DISPLAY_ID + "]prox negative");
         assertEquals(mWakelockController.getSuspendBlockerProxDebounceId(),
                 "[" + DISPLAY_ID + "]prox debounce");
+        assertEquals(mWakelockController.getSuspendBlockerOverrideDozeScreenState(),
+                "[" + DISPLAY_ID + "]override doze screen state");
     }
 
     @Test
@@ -162,6 +164,28 @@
     }
 
     @Test
+    public void acquireOverrideDozeScreenStateSuspendBlocker() throws Exception {
+        // Acquire the suspend blocker
+        verifyWakelockAcquisitionAndReaquisition(WakelockController
+                        .WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE,
+                () -> mWakelockController.isOverrideDozeScreenStateAcquired());
+
+        // Verify acquire happened only once
+        verify(mDisplayPowerCallbacks, times(1))
+                .acquireSuspendBlocker(mWakelockController
+                        .getSuspendBlockerOverrideDozeScreenState());
+
+        // Release the suspend blocker
+        verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_OVERRIDE_DOZE_SCREEN_STATE,
+                () -> mWakelockController.isOverrideDozeScreenStateAcquired());
+
+        // Verify suspend blocker was released only once
+        verify(mDisplayPowerCallbacks, times(1))
+                .releaseSuspendBlocker(mWakelockController
+                        .getSuspendBlockerOverrideDozeScreenState());
+    }
+
+    @Test
     public void proximityPositiveRunnableWorksAsExpected() {
         // Acquire the suspend blocker twice
         assertTrue(mWakelockController.acquireWakelock(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
index 0ed96ae..bb025cc 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
@@ -16,7 +16,10 @@
 
 package com.android.server.display.brightness.clamper
 
+import android.content.Context
+import android.database.ContentObserver
 import android.hardware.display.DisplayManagerInternal
+import android.net.Uri
 import android.os.IBinder
 import android.os.PowerManager.BRIGHTNESS_MAX
 import android.util.Spline
@@ -51,7 +54,7 @@
 
     private val stoppedClock = OffsettableClock.Stopped()
     private val testHandler = TestHandler(null, stoppedClock)
-    private val testInjector = TestInjector()
+    private val testInjector = TestInjector(mock<Context>())
     private val mockChangeListener = mock<ClamperChangeListener>()
     private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>()
     private val mockDisplayBinder = mock<IBinder>()
@@ -63,14 +66,14 @@
     private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder)
 
     @Test
-    fun `change listener is not called on init`() {
+    fun changeListenerIsNotCalledOnInit() {
         initHdrModifier()
 
         verify(mockChangeListener, never()).onChanged()
     }
 
     @Test
-    fun `hdr listener registered on init if hdr data is present`() {
+    fun hdrListenerRegisteredOnInit_hdrDataPresent() {
         initHdrModifier()
 
         assertThat(testInjector.registeredHdrListener).isNotNull()
@@ -78,22 +81,19 @@
     }
 
     @Test
-    fun `hdr listener not registered on init if hdr data is missing`() {
-        initHdrModifier(null)
-
-        testHandler.flush()
+    fun hdrListenerNotRegisteredOnInit_hdrDataMissing() {
+        initHdrModifier(hdrBrightnessData = null)
 
         assertThat(testInjector.registeredHdrListener).isNull()
         assertThat(testInjector.registeredToken).isNull()
     }
 
     @Test
-    fun `unsubscribes hdr listener when display changed with no hdr data`() {
+    fun unsubscribeHdrListener_displayChangedWithNoHdrData() {
         initHdrModifier()
 
         whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
         modifier.onDisplayChanged(dummyData)
-        testHandler.flush()
 
         assertThat(testInjector.registeredHdrListener).isNull()
         assertThat(testInjector.registeredToken).isNull()
@@ -101,12 +101,11 @@
     }
 
     @Test
-    fun `resubscribes hdr listener when display changed with different token`() {
+    fun resubscribesHdrListener_displayChangedWithDifferentToken() {
         initHdrModifier()
 
         modifier.onDisplayChanged(
             createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther))
-        testHandler.flush()
 
         assertThat(testInjector.registeredHdrListener).isNotNull()
         assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther)
@@ -114,7 +113,28 @@
     }
 
     @Test
-    fun `test NO_HDR mode`() {
+    fun contentObserverNotRegisteredOnInit_hdrDataMissing() {
+        initHdrModifier(null)
+
+        assertThat(testInjector.registeredContentObserver).isNull()
+    }
+
+    @Test
+    fun contentObserverNotRegisteredOnInit_allowedInLowPowerMode() {
+        initHdrModifier(createHdrBrightnessData(allowInLowPowerMode = true))
+
+        assertThat(testInjector.registeredContentObserver).isNull()
+    }
+
+    @Test
+    fun contentObserverRegisteredOnInit_notAllowedInLowPowerMode() {
+        initHdrModifier(createHdrBrightnessData(allowInLowPowerMode = false))
+
+        assertThat(testInjector.registeredContentObserver).isNotNull()
+    }
+
+    @Test
+    fun testNoHdrMode() {
         initHdrModifier()
         // screen size = 10_000
         setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
@@ -131,7 +151,7 @@
     }
 
     @Test
-    fun `test NBM_HDR mode`() {
+    fun testNbmHdrMode() {
         initHdrModifier()
         // screen size = 10_000
         val transitionPoint = 0.55f
@@ -157,7 +177,7 @@
     }
 
     @Test
-    fun `test HBM_HDR mode`() {
+    fun testHbmHdrMode() {
         initHdrModifier()
         // screen size = 10_000
         setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
@@ -182,7 +202,7 @@
     }
 
     @Test
-    fun `test display change no HDR content`() {
+    fun testDisplayChange_noHdrContent() {
         initHdrModifier()
         setupDisplay(width = 100, height = 100)
         assertModifierState()
@@ -195,7 +215,7 @@
     }
 
     @Test
-    fun `test display change with HDR content`() {
+    fun testDisplayChange_hdrContent() {
         initHdrModifier()
         setupDisplay(width = 100, height = 100)
         setupHdrLayer(width = 100, height = 100, maxHdrRatio = 5f)
@@ -218,7 +238,7 @@
     }
 
     @Test
-    fun `test ambient lux decrease above maxBrightnessLimits no HDR`() {
+    fun testSetAmbientLux_decreaseAboveMaxBrightnessLimitNoHdr() {
         initHdrModifier()
         modifier.setAmbientLux(1000f)
         setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
@@ -234,7 +254,7 @@
     }
 
     @Test
-    fun `test ambient lux decrease above maxBrightnessLimits with HDR`() {
+    fun testSetAmbientLux_decreaseAboveMaxBrightnessLimitWithHdr() {
         initHdrModifier()
         modifier.setAmbientLux(1000f)
         setupDisplay(width = 200, height = 200, hdrBrightnessData = createHdrBrightnessData(
@@ -260,7 +280,7 @@
     }
 
     @Test
-    fun `test ambient lux decrease below maxBrightnessLimits no HDR`() {
+    fun testSetAmbientLux_decreaseBelowMaxBrightnessLimitNoHdr() {
         initHdrModifier()
         modifier.setAmbientLux(1000f)
         setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
@@ -276,7 +296,7 @@
     }
 
     @Test
-    fun `test ambient lux decrease below maxBrightnessLimits with HDR`() {
+    fun testSetAmbientLux_decreaseBelowMaxBrightnessLimitWithHdr() {
         initHdrModifier()
         modifier.setAmbientLux(1000f)
         val maxBrightness = 0.6f
@@ -322,6 +342,23 @@
         )
     }
 
+    @Test
+    fun testLowPower_notAllowedInLowPower() {
+        initHdrModifier()
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            allowInLowPowerMode = false
+        ))
+        setupHdrLayer(width = 100, height = 100)
+        clearInvocations(mockChangeListener)
+
+        testInjector.isLowPower = true
+        testInjector.registeredContentObserver!!.onChange(true)
+
+        verify(mockChangeListener).onChanged()
+        assertModifierState()
+    }
+
+    // Helper functions
     private fun setupHdrLayer(width: Int = 100, height: Int = 100, maxHdrRatio: Float = 0.8f) {
         testInjector.registeredHdrListener!!.onHdrInfoChanged(
             mockDisplayBinder, 1, width, height, 0, maxHdrRatio
@@ -345,7 +382,6 @@
             width = width,
             height = height
         ))
-        testHandler.flush()
     }
 
     private fun initHdrModifier(hdrBrightnessData: HdrBrightnessData? = createHdrBrightnessData()) {
@@ -384,9 +420,12 @@
         assertThat(stateBuilder.customAnimationRate).isEqualTo(animationRate)
     }
 
-    internal class TestInjector : Injector() {
+    internal class TestInjector(context: Context) : Injector(context) {
         var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null
         var registeredToken: IBinder? = null
+        var registeredContentObserver: ContentObserver? = null
+
+        var isLowPower: Boolean = false
 
         override fun registerHdrListener(
             listener: SurfaceControlHdrLayerInfoListener, token: IBinder
@@ -401,5 +440,17 @@
             registeredHdrListener = null
             registeredToken = null
         }
+
+        override fun registerContentObserver(observer: ContentObserver, uri: Uri) {
+            registeredContentObserver = observer
+        }
+
+        override fun unregisterContentObserver(observer: ContentObserver) {
+            registeredContentObserver = null
+        }
+
+        override fun isLowPowerMode(): Boolean {
+            return isLowPower
+        }
     }
 }
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/jni/Android.bp b/services/tests/mockingservicestests/jni/Android.bp
index 1eb9888..00543a8 100644
--- a/services/tests/mockingservicestests/jni/Android.bp
+++ b/services/tests/mockingservicestests/jni/Android.bp
@@ -21,6 +21,7 @@
 
     srcs: [
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         "onload.cpp",
diff --git a/services/tests/mockingservicestests/jni/onload.cpp b/services/tests/mockingservicestests/jni/onload.cpp
index fb91051..cb246d1 100644
--- a/services/tests/mockingservicestests/jni/onload.cpp
+++ b/services/tests/mockingservicestests/jni/onload.cpp
@@ -25,6 +25,7 @@
 
 namespace android {
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
+int register_android_server_am_Freezer(JNIEnv* env);
 int register_android_server_app_GameManagerService(JNIEnv* env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 };
@@ -42,8 +43,8 @@
     }
     ALOG_ASSERT(env, "Could not retrieve the env!");
     register_android_server_am_CachedAppOptimizer(env);
+    register_android_server_am_Freezer(env);
     register_android_server_app_GameManagerService(env);
     register_android_server_am_OomConnection(env);
     return JNI_VERSION_1_4;
 }
-
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index cb15d6f..b980ca0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -132,6 +132,7 @@
 import android.content.Intent;
 import android.content.PermissionChecker;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserInfo;
 import android.net.Uri;
 import android.os.BatteryManager;
 import android.os.Bundle;
@@ -149,6 +150,7 @@
 import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
@@ -176,6 +178,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.permission.PermissionManagerService;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -250,6 +253,8 @@
     @Mock
     private ActivityManagerInternal mActivityManagerInternal;
     @Mock
+    private UserManagerInternal mUserManagerInternal;
+    @Mock
     private ActivityManager mActivityManager;
     @Mock
     private PackageManagerInternal mPackageManagerInternal;
@@ -447,6 +452,8 @@
                 () -> LocalServices.getService(PermissionManagerServiceInternal.class));
         doReturn(mActivityManagerInternal).when(
                 () -> LocalServices.getService(ActivityManagerInternal.class));
+        doReturn(mUserManagerInternal).when(
+                () -> LocalServices.getService(UserManagerInternal.class));
         doReturn(mPackageManagerInternal).when(
                 () -> LocalServices.getService(PackageManagerInternal.class));
         doReturn(mAppStateTracker).when(() -> LocalServices.getService(AppStateTracker.class));
@@ -1252,6 +1259,26 @@
     }
 
     @Test
+    public void wakeupShouldBeScheduledForFullUsers_skipsGuestSystemAndProfiles() {
+        final int systemUserId = 0;
+        final int fullUserId = 10;
+        final int privateProfileId = 12;
+        final int guestUserId = 13;
+        when(mUserManagerInternal.getUserInfo(fullUserId)).thenReturn(new UserInfo(fullUserId,
+                "TestUser2", UserInfo.FLAG_FULL));
+        when(mUserManagerInternal.getUserInfo(privateProfileId)).thenReturn(new UserInfo(
+                privateProfileId, "TestUser3", UserInfo.FLAG_PROFILE));
+        when(mUserManagerInternal.getUserInfo(guestUserId)).thenReturn(new UserInfo(
+                guestUserId, "TestUserGuest", null, 0, UserManager.USER_TYPE_FULL_GUEST));
+        when(mUserManagerInternal.getUserInfo(systemUserId)).thenReturn(new UserInfo(
+                systemUserId, "TestUserSystem", null, 0, UserManager.USER_TYPE_FULL_SYSTEM));
+        assertTrue(mService.shouldAddWakeupForUser(fullUserId));
+        assertFalse(mService.shouldAddWakeupForUser(systemUserId));
+        assertFalse(mService.shouldAddWakeupForUser(privateProfileId));
+        assertFalse(mService.shouldAddWakeupForUser(guestUserId));
+    }
+
+    @Test
     public void sendsTimeTickOnInteractive() {
         final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
         // Stubbing so the handler doesn't actually run the runnable.
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
index 5bd919f..72883e2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -23,7 +23,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.testng.AssertJUnit.assertFalse;
 
 import android.os.Environment;
 import android.os.FileUtils;
@@ -52,7 +51,6 @@
     private static final int USER_ID_1 = 10;
     private static final int USER_ID_2 = 11;
     private static final int USER_ID_3 = 12;
-    private static final int USER_ID_SYSTEM = 0;
     private static final long TEST_TIMESTAMP = 150_000;
     private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
             .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
@@ -112,14 +110,6 @@
     }
 
     @Test
-    public void testAddWakeupForSystemUser_shouldDoNothing() {
-        mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000);
-        assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
-        final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
-        assertFalse(file.exists());
-    }
-
-    @Test
     public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
         final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
         mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
index 03439e55..32ff569 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
@@ -22,8 +22,16 @@
 
 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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -33,12 +41,14 @@
 import android.os.HandlerThread;
 import android.os.MessageQueue;
 import android.os.Process;
+import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
 import android.text.TextUtils;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.modules.utils.testing.TestableDeviceConfig;
 import com.android.server.LocalServices;
@@ -55,9 +65,11 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -79,12 +91,17 @@
     private CountDownLatch mCountDown;
     private ActivityManagerService mAms;
     private Context mContext;
+    private TestFreezer mFreezer;
+    private CountDownLatch mFreezeCounter;
     private TestInjector mInjector;
     private TestProcessDependencies mProcessDependencies;
 
     @Mock
     private PackageManagerInternal mPackageManagerInt;
 
+    // Control whether the freezer mock reports that freezing is enabled or not.
+    private boolean mUseFreezer;
+
     @Rule
     public final ApplicationExitInfoTest.ServiceThreadRule
             mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
@@ -103,9 +120,12 @@
                 true /* allowIo */);
         mThread.start();
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+        mUseFreezer = false;
+        mFreezer = new TestFreezer();
+
         mInjector = new TestInjector(mContext);
-        mAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
+        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
         doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
         mProcessDependencies = new TestProcessDependencies();
         mCachedAppOptimizerUnderTest = new CachedAppOptimizer(mAms,
@@ -126,6 +146,7 @@
         mHandlerThread.quit();
         mThread.quit();
         mCountDown = null;
+        mFreezeCounter = null;
     }
 
     private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, String processName,
@@ -179,7 +200,7 @@
         assertThat(mCachedAppOptimizerUnderTest.mProcStateThrottle)
                 .containsExactlyElementsIn(expected);
 
-        Assume.assumeTrue(mCachedAppOptimizerUnderTest.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isEqualTo(
                 CachedAppOptimizer.DEFAULT_USE_FREEZER);
     }
@@ -265,8 +286,8 @@
                 CachedAppOptimizer.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1);
         assertThat(mCachedAppOptimizerUnderTest.mProcStateThrottle).containsExactly(1, 2, 3);
 
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
-        if (CachedAppOptimizer.isFreezerSupported()) {
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
+        if (mAms.isAppFreezerSupported()) {
             if (CachedAppOptimizer.DEFAULT_USE_FREEZER) {
                 assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
             } else {
@@ -300,7 +321,7 @@
 
     @Test
     public void useFreeze_doesNotListenToDeviceConfigChanges() throws InterruptedException {
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
 
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
 
@@ -353,7 +374,7 @@
 
     @Test
     public void useFreeze_listensToDeviceConfigChangesBadValues() throws InterruptedException {
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
 
         // When we push an invalid flag value...
@@ -982,6 +1003,40 @@
         }
     }
 
+    @Test
+    public void testFreezerDelegator() throws Exception {
+        mUseFreezer = true;
+        mProcessDependencies.setRss(new long[] {
+                    0 /*total_rss*/,
+                    0 /*file*/,
+                    0 /*anon*/,
+                    0 /*swap*/,
+                    0 /*shmem*/
+                });
+
+        // Force the system to use the freezer
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                CachedAppOptimizer.KEY_USE_FREEZER, "true", false);
+        mCachedAppOptimizerUnderTest.init();
+        initActivityManagerService();
+
+        assertTrue(mAms.isAppFreezerSupported());
+        assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isTrue();
+
+        int pid = 10000;
+        int uid = 2;
+        int pkgUid = 3;
+        ProcessRecord app = makeProcessRecord(pid, uid, pkgUid, "p1", "app1");
+
+        mFreezeCounter = new CountDownLatch(1);
+        mCachedAppOptimizerUnderTest.forceFreezeForTest(app, true);
+        assertTrue(mFreezeCounter.await(5, TimeUnit.SECONDS));
+
+        mFreezeCounter = new CountDownLatch(1);
+        mCachedAppOptimizerUnderTest.forceFreezeForTest(app, false);
+        assertTrue(mFreezeCounter.await(5, TimeUnit.SECONDS));
+    }
+
     private void setFlag(String key, String value, boolean defaultValue) throws Exception {
         mCountDown = new CountDownLatch(1);
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, key, value, defaultValue);
@@ -1042,6 +1097,11 @@
         public Handler getUiHandler(ActivityManagerService service) {
             return mHandler;
         }
+
+        @Override
+        public Freezer getFreezer() {
+            return mFreezer;
+        }
     }
 
     // Test implementation for ProcessDependencies.
@@ -1069,4 +1129,27 @@
             mRssAfterCompaction = newValues;
         }
     }
+
+    // Intercept Freezer calls.
+    private class TestFreezer extends Freezer {
+        @Override
+        public void setProcessFrozen(int pid, int uid, boolean frozen) {
+            mFreezeCounter.countDown();
+        }
+
+        @Override
+        public int freezeBinder(int pid, boolean freeze, int timeoutMs) {
+            return 0;
+        }
+
+        @Override
+        public int getBinderFreezeInfo(int pid) {
+            return 0;
+        }
+
+        @Override
+        public boolean isFreezerSupported() {
+            return mUseFreezer;
+        }
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/OWNERS b/services/tests/mockingservicestests/src/com/android/server/am/OWNERS
index 2cbc226..4fac647 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/am/OWNERS
@@ -1,3 +1,4 @@
 include /services/core/java/com/android/server/am/OWNERS
 
 per-file ApplicationStartInfoTest.java = yforta@google.com, carmenjackson@google.com, jji@google.com
+per-file CachedAppOptimizerTest.java = file:/PERFORMANCE_OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index 3572d23..014b98c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -56,6 +56,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -238,6 +239,7 @@
     /**
      * Verify that a process start event is dispatched to process observers.
      */
+    @Ignore("b/323959187")
     @Test
     public void testNormal() throws Exception {
         ProcessRecord app = startProcess();
diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp
index c30e4eb..0a31037 100644
--- a/services/tests/servicestests/jni/Android.bp
+++ b/services/tests/servicestests/jni/Android.bp
@@ -21,6 +21,7 @@
 
     srcs: [
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         ":lib_anrTimer_native",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 7b71f85..1426d5d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -48,6 +48,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -67,6 +68,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.R;
 import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
@@ -118,6 +120,7 @@
     final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
             mock(FullScreenMagnificationController.ControllerContext.class);
     final Context mMockContext = mock(Context.class);
+    final Resources mMockResources = mock(Resources.class);
     final AccessibilityTraceManager mMockTraceManager = mock(AccessibilityTraceManager.class);
     final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class);
     private final MagnificationAnimationCallback mAnimationCallback = mock(
@@ -162,6 +165,7 @@
         mResolver = new MockContentResolver();
         mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
         when(mMockContext.getContentResolver()).thenReturn(mResolver);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
         mOriginalMagnificationPersistedScale = Settings.Secure.getFloatForUser(mResolver,
                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
                 CURRENT_USER_ID);
@@ -928,7 +932,8 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ true,
                     /* isAlwaysOnEnabled= */ false,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
@@ -940,7 +945,24 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ true,
                     /* isAlwaysOnEnabled= */ true,
-                    /* expectedActivated= */ true);
+                    /* expectedActivated= */ true,
+                    /* expectedMagnified= */ false);
+            resetMockWindowManager();
+        }
+    }
+
+    @Test
+    public void testUserContextChange_magnifierActivatedAndKeepMagnifiedEnabled_stayActivated() {
+        when(mMockResources.getBoolean(
+                R.bool.config_magnification_keep_zoom_level_when_context_changed))
+                .thenReturn(true);
+        for (int i = 0; i < DISPLAY_COUNT; i++) {
+            contextChange_expectedValues(
+                    /* displayId= */ i,
+                    /* isMagnifierActivated= */ true,
+                    /* isAlwaysOnEnabled= */ true,
+                    /* expectedActivated= */ true,
+                    /* expectedMagnified= */ true);
             resetMockWindowManager();
         }
     }
@@ -952,7 +974,8 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ false,
                     /* isAlwaysOnEnabled= */ false,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
@@ -964,14 +987,15 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ false,
                     /* isAlwaysOnEnabled= */ true,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
 
     private void contextChange_expectedValues(
             int displayId, boolean isMagnifierActivated, boolean isAlwaysOnEnabled,
-            boolean expectedActivated) {
+            boolean expectedActivated, boolean expectedMagnified) {
         mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(isAlwaysOnEnabled);
         register(displayId);
         MagnificationCallbacks callbacks = getMagnificationCallbacks(displayId);
@@ -982,7 +1006,7 @@
         callbacks.onUserContextChanged();
         mMessageCapturingHandler.sendAllMessages();
         checkActivatedAndMagnifying(
-                /* activated= */ expectedActivated, /* magnifying= */ false, displayId);
+                /* activated= */ expectedActivated, expectedMagnified, displayId);
 
         if (expectedActivated) {
             verify(mMockThumbnail, times(2)).setThumbnailBounds(
@@ -1526,8 +1550,8 @@
     private void checkActivatedAndMagnifying(boolean activated, boolean magnifying, int displayId) {
         final boolean isActivated = mFullScreenMagnificationController.isActivated(displayId);
         final boolean isMagnifying = mFullScreenMagnificationController.getScale(displayId) > 1.0f;
-        assertTrue(isActivated == activated);
-        assertTrue(isMagnifying == magnifying);
+        assertEquals(isActivated, activated);
+        assertEquals(isMagnifying, magnifying);
     }
 
     private MagnificationCallbacks getMagnificationCallbacks(int displayId) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index 2d4dbb7..78c9372 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -479,6 +479,15 @@
                 eq(mContext.getOpPackageName()), anyInt(), anyInt(), any());
     }
 
+    @Test
+    public void testCancelAuth_whenClientWaitingForCookie() throws RemoteException {
+        final FaceAuthenticationClient client = createClient(true);
+        client.waitForCookie(mCallback);
+        client.cancel();
+
+        verify(mCallback).onClientFinished(client, false);
+    }
+
     private FaceAuthenticationClient createClient() throws RemoteException {
         return createClient(2 /* version */, mClientMonitorCallbackConverter,
                 false /* allowBackgroundAuthentication */, true /* isBiometricPrompt */,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 6ec888c..7e1d421 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -117,7 +117,6 @@
     private static final int TOUCH_Y = 20;
     private static final float TOUCH_MAJOR = 4.4f;
     private static final float TOUCH_MINOR = 5.5f;
-    private static final int FINGER_UP = 111;
 
     @Rule
     public final TestableContext mContext = new TestableContext(
@@ -383,6 +382,8 @@
 
     @Test
     public void subscribeContextAndStartHal() throws RemoteException {
+        when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal);
+
         final FingerprintAuthenticationClient client = createClient();
         client.start(mCallback);
 
@@ -691,6 +692,17 @@
         verify(mLockoutTracker).addFailedAttemptForUser(USER_ID);
     }
 
+    @Test
+    public void testCancelAuth_whenClientWaitingForCookie() throws RemoteException {
+        final FingerprintAuthenticationClient client = createClientWithoutBackgroundAuth();
+        client.waitForCookie(mCallback);
+        client.cancel();
+        mLooper.moveTimeForward(10);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(client, false);
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */,
                 true /* isBiometricPrompt */,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
index a34e796..242880c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
@@ -178,7 +178,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NOTIFY_FINGERPRINT_LOE)
+    @RequiresFlagsEnabled(Flags.FLAG_NOTIFY_FINGERPRINTS_LOE)
     public void invalidBiometricUserState() throws Exception {
         mClient =  createClient();
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
index e078238..7eabfac 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
@@ -518,19 +518,6 @@
     }
 
     @Test
-    public void canActivityBeLaunched_permissionComponent_isBlocked() {
-        GenericWindowPolicyController gwpc = createGwpcWithPermissionComponent(BLOCKED_COMPONENT);
-        gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
-
-        ActivityInfo activityInfo = getActivityInfo(
-                BLOCKED_PACKAGE_NAME,
-                BLOCKED_PACKAGE_NAME,
-                /* displayOnRemoteDevices */ true,
-                /* targetDisplayCategory */ null);
-        assertActivityIsBlocked(gwpc, activityInfo);
-    }
-
-    @Test
     public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() {
         ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(TEST_UID));
         GenericWindowPolicyController gwpc = createGwpc();
@@ -740,7 +727,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ mSecureWindowCallback,
@@ -760,7 +746,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ mSecureWindowCallback,
@@ -781,7 +766,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
@@ -802,7 +786,6 @@
                 /* activityPolicyExemptions= */ Collections.singleton(blockedComponent),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
@@ -823,7 +806,6 @@
                 /* activityPolicyExemptions= */ Collections.singleton(allowedComponent),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
@@ -844,7 +826,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
@@ -865,7 +846,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ true,
                 /* crossTaskNavigationExemptions= */ Collections.singleton(blockedComponent),
-                /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
@@ -886,29 +866,6 @@
                 /* activityPolicyExemptions= */ new ArraySet<>(),
                 /* crossTaskNavigationAllowedByDefault= */ false,
                 /* crossTaskNavigationExemptions= */ Collections.singleton(allowedComponent),
-                /* permissionDialogComponent= */ null,
-                /* activityListener= */ mActivityListener,
-                /* activityBlockedCallback= */ mActivityBlockedCallback,
-                /* secureWindowCallback= */ null,
-                /* intentListenerCallback= */ mIntentListenerCallback,
-                /* displayCategories= */ new ArraySet<>(),
-                /* showTasksInHostDeviceRecents= */ true,
-                /* customHomeComponent= */ null);
-    }
-
-    private GenericWindowPolicyController createGwpcWithPermissionComponent(
-            ComponentName permissionComponent) {
-        //TODO instert the component
-        return new GenericWindowPolicyController(
-                0,
-                0,
-                AttributionSource.myAttributionSource(),
-                /* allowedUsers= */ new ArraySet<>(getCurrentUserId()),
-                /* activityLaunchAllowedByDefault= */ true,
-                /* activityPolicyExemptions= */ new ArraySet<>(),
-                /* crossTaskNavigationAllowedByDefault= */ false,
-                /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                /* permissionDialogComponent= */ permissionComponent,
                 /* activityListener= */ mActivityListener,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index da8961d..19712ea 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -1550,8 +1550,7 @@
     }
 
     @Test
-    public void openPermissionControllerOnVirtualDisplay_displayOnRemoteDevices_startsWhenFlagIsEnabled() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+    public void openPermissionControllerOnVirtualDisplay_displayOnRemoteDevices_starts() {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
         GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
                 DISPLAY_ID_1);
@@ -1572,8 +1571,7 @@
     }
 
     @Test
-    public void openPermissionControllerOnVirtualDisplay_dontDisplayOnRemoteDevices_startsWhenFlagIsEnabled() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+    public void openPermissionControllerOnVirtualDisplay_dontDisplayOnRemoteDevices_starts() {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
         GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
                 DISPLAY_ID_1);
@@ -1755,35 +1753,7 @@
     }
 
     @Test
-    public void canActivityBeLaunched_permissionDialog_flagDisabled_isBlocked() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_STREAM_PERMISSIONS);
-        VirtualDeviceParams params = new VirtualDeviceParams.Builder().build();
-        mDeviceImpl.close();
-        mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
-        doNothing().when(mContext).startActivityAsUser(any(), any(), any());
-
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
-                DISPLAY_ID_1);
-        ComponentName permissionComponent = getPermissionDialogComponent();
-        ActivityInfo activityInfo = getActivityInfo(
-                permissionComponent.getPackageName(),
-                permissionComponent.getClassName(),
-                /* displayOnRemoteDevices */ true,
-                /* targetDisplayCategory */ null);
-        assertThat(gwpc.canActivityBeLaunched(activityInfo, null,
-                WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false))
-                .isFalse();
-
-        Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent(
-                activityInfo, mAssociationInfo.getDisplayName());
-        verify(mContext).startActivityAsUser(argThat(intent ->
-                intent.filterEquals(blockedAppIntent)), any(), any());
-    }
-
-    @Test
-    public void canActivityBeLaunched_permissionDialog_flagEnabled_isStreamed() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+    public void canActivityBeLaunched_permissionDialog_isStreamed() {
         VirtualDeviceParams params = new VirtualDeviceParams.Builder().build();
         mDeviceImpl.close();
         mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index b946a43..c3db396 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -85,7 +85,6 @@
                         /* activityPolicyExemptions= */ new ArraySet<>(),
                         /* crossTaskNavigationAllowedByDefault= */ true,
                         /* crossTaskNavigationExemptions= */ new ArraySet<>(),
-                        /* permissionDialogComponent */ null,
                         /* activityListener= */ null,
                         /* activityBlockedCallback= */ null,
                         /* secureWindowCallback= */ null,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index e06d939..de70280 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2454,6 +2454,50 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_Avalanche_exemptCategories() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        // Trigger avalanche trigger intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", false);
+        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+        // CATEGORY_ALARM is exempted
+        NotificationRecord r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_ALARM;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+
+        // CATEGORY_CAR_EMERGENCY is exempted
+        Mockito.reset(mRingtonePlayer);
+        NotificationRecord r2 = getBeepyNotification();
+        r2.getNotification().category = Notification.CATEGORY_CAR_EMERGENCY;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
+
+        // CATEGORY_CAR_WARNING is exempted
+        Mockito.reset(mRingtonePlayer);
+        NotificationRecord r3 = getBeepyNotification();
+        r3.getNotification().category = Notification.CATEGORY_CAR_WARNING;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+
+        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_exemptEmergency() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
@@ -2492,6 +2536,73 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_exemptCategories() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        // CATEGORY_ALARM is exempted
+        NotificationRecord r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_ALARM;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // CATEGORY_CAR_WARNING is exempted
+        r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_CAR_WARNING;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // CATEGORY_CAR_EMERGENCY is exempted
+        r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_CAR_EMERGENCY;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        verify(mAccessibilityService, times(9)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_applyPerApp() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
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 f7340ab..3c3c2f3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -19,12 +19,18 @@
 import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.app.Flags.FLAG_MODES_UI;
 import static android.app.Flags.modesUi;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
+import static android.app.NotificationManager.Policy.suppressedEffectsToString;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.Condition.SOURCE_UNKNOWN;
 import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
@@ -219,8 +225,8 @@
         priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_LIGHTS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_AMBIENT;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_LIGHTS;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_AMBIENT;
 
         Policy expectedPolicy = new Policy(priorityCategories, priorityCallSenders,
                 priorityMessageSenders, suppressedVisualEffects, 0, priorityConversationsSenders);
@@ -256,8 +262,8 @@
         priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_LIGHTS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_AMBIENT;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_LIGHTS;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_AMBIENT;
 
         Policy expectedPolicy = new Policy(priorityCategories, priorityCallSenders,
                 priorityMessageSenders, suppressedVisualEffects,
@@ -309,8 +315,8 @@
             config.setAllowMessagesFrom(Policy.PRIORITY_SENDERS_STARRED);
             config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
             config.setSuppressedVisualEffects(config.getSuppressedVisualEffects()
-                    | Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS
-                    | Policy.SUPPRESSED_EFFECT_AMBIENT);
+                    | Policy.SUPPRESSED_EFFECT_BADGE | SUPPRESSED_EFFECT_LIGHTS
+                    | SUPPRESSED_EFFECT_AMBIENT);
         }
         ZenPolicy actual = config.getZenPolicy();
 
@@ -357,8 +363,8 @@
             config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
             config.setAllowPriorityChannels(false);
             config.setSuppressedVisualEffects(config.getSuppressedVisualEffects()
-                    | Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS
-                    | Policy.SUPPRESSED_EFFECT_AMBIENT);
+                    | Policy.SUPPRESSED_EFFECT_BADGE | SUPPRESSED_EFFECT_LIGHTS
+                    | SUPPRESSED_EFFECT_AMBIENT);
         }
         ZenPolicy actual = config.getZenPolicy();
 
@@ -1063,6 +1069,43 @@
                 .isEqualTo("name");
     }
 
+    @Test
+    public void toNotificationPolicy_withNewSuppressedEffects_returnsSuppressedEffects() {
+        ZenModeConfig config = getCustomConfig();
+        // From LegacyNotificationManagerTest.testSetNotificationPolicy_preP_setNewFields
+        // When a pre-P app sets SUPPRESSED_EFFECT_NOTIFICATION_LIST, it's converted by NMS into:
+        Policy policy = new Policy(0, 0, 0,
+                SUPPRESSED_EFFECT_FULL_SCREEN_INTENT | SUPPRESSED_EFFECT_LIGHTS
+                        | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT);
+
+        config.applyNotificationPolicy(policy);
+        Policy result = config.toNotificationPolicy();
+
+        assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
+    }
+
+    @Test
+    public void toNotificationPolicy_withOldAndNewSuppressedEffects_returnsSuppressedEffects() {
+        ZenModeConfig config = getCustomConfig();
+        // From LegacyNotificationManagerTest.testSetNotificationPolicy_preP_setOldNewFields.
+        // When a pre-P app sets SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_STATUS_BAR, it's
+        // converted by NMS into:
+        Policy policy = new Policy(0, 0, 0,
+                SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
+                        | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK
+                        | SUPPRESSED_EFFECT_AMBIENT);
+
+        config.applyNotificationPolicy(policy);
+        Policy result = config.toNotificationPolicy();
+
+        assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
+    }
+
+    private static String suppressedEffectsOf(Policy policy) {
+        return suppressedEffectsToString(policy.suppressedVisualEffects) + "("
+                + policy.suppressedVisualEffects + ")";
+    }
+
     private ZenModeConfig getMutedRingerConfig() {
         ZenModeConfig config = new ZenModeConfig();
 
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 57587f7..9af0021 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -61,6 +61,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -80,19 +81,6 @@
                     ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
                     : Set.of("version", "manualRule", "automaticRules");
 
-    // Differences for flagged fields are only generated if the flag is enabled.
-    // "Metadata" fields (userModifiedFields, deletionInstant, disabledOrigin) are not compared.
-    private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
-            android.app.Flags.modesApi()
-                    ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
-                            "zenDeviceEffectsUserModifiedFields", "deletionInstant",
-                            "disabledOrigin")
-                    : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
-                            RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
-                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
-                            "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields",
-                            "deletionInstant", "disabledOrigin");
-
     // allowPriorityChannels is flagged by android.app.modes_api
     public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
             Set.of("allowPriorityChannels");
@@ -102,8 +90,7 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return FlagsParameterization.allCombinationsOf(
-                FLAG_MODES_UI);
+        return FlagsParameterization.progressionOf(FLAG_MODES_API, FLAG_MODES_UI);
     }
 
     public ZenModeDiffTest(FlagsParameterization flags) {
@@ -140,7 +127,7 @@
         ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
         ArrayMap<String, Object> expectedTo = new ArrayMap<>();
         List<Field> fieldsForDiff = getFieldsForDiffCheck(
-                ZenModeConfig.ZenRule.class, ZEN_RULE_EXEMPT_FIELDS);
+                ZenModeConfig.ZenRule.class, getZenRuleExemptFields());
         generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
 
         ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
@@ -158,6 +145,25 @@
         }
     }
 
+    private static Set<String> getZenRuleExemptFields() {
+        // "Metadata" fields are never compared.
+        Set<String> exemptFields = new LinkedHashSet<>(
+                Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
+                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin"));
+        // 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())) {
+            exemptFields.add(RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS);
+        }
+        return exemptFields;
+    }
+
     @Test
     public void testConfigDiff_addRemoveSame() {
         // Default config, will test add, remove, and no change
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
index 50041d0..d147325 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
@@ -35,6 +35,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.os.Handler;
@@ -89,9 +90,12 @@
             testActivityInfo.applicationInfo = new ApplicationInfo();
             testActivityInfo.packageName =
                     testActivityInfo.applicationInfo.packageName = "com.test";
+            ResolveInfo testResolveInfo = new ResolveInfo();
+            testResolveInfo.activityInfo = testActivityInfo;
 
             doReturn(testActivityInfo).when(mPackageManager).getActivityInfo(
                     eq(new ComponentName("com.test", "com.test.BookmarkTest")), anyInt());
+            doReturn(testResolveInfo).when(mPackageManager).resolveActivity(anyObject(), anyInt());
             doThrow(new PackageManager.NameNotFoundException("com.test3")).when(mPackageManager)
                     .getActivityInfo(eq(new ComponentName("com.test3", "com.test.BookmarkTest")),
                         anyInt());
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index 2e85025..9b92ff4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -36,6 +36,7 @@
 import android.provider.Settings;
 import android.view.Display;
 
+import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -48,6 +49,13 @@
 
     private static final String TEST_TARGET_ACTIVITY = "com.android.server.policy/.TestActivity";
 
+    @Before
+    public void setup() {
+        super.setup();
+        overrideResource(com.android.internal.R.integer.config_longPressOnStemPrimaryBehavior,
+                LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT);
+    }
+
     /**
      * Stem single key should not launch behavior during set up.
      */
@@ -186,6 +194,26 @@
     }
 
     @Test
+    public void stemLongKey_appHasOverridePermission_consumedByApp_triggerStatusBarToStartAssist() {
+        overrideBehavior(
+                STEM_PRIMARY_BUTTON_LONG_PRESS,
+                LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT);
+        setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
+        mPhoneWindowManager.setupAssistForLaunch();
+        mPhoneWindowManager.overrideSearchManager(null);
+        mPhoneWindowManager.overrideStatusBarManagerInternal();
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+        mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(true);
+
+        setDispatchedKeyHandler(keyEvent -> true);
+
+        sendKey(KEYCODE_STEM_PRIMARY, /* longPress= */ true);
+
+        mPhoneWindowManager.assertStatusBarStartAssist();
+    }
+
+    @Test
     public void stemDoubleKey_EarlyShortPress_AllAppsThenSwitchToMostRecent()
             throws RemoteException {
         overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index a159ce3..44837d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -58,6 +58,7 @@
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.util.ArraySet;
 import android.view.WindowManager;
 import android.window.BackAnimationAdapter;
@@ -72,6 +73,7 @@
 import android.window.WindowOnBackInvokedDispatcher;
 
 import com.android.server.LocalServices;
+import com.android.window.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -612,6 +614,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION)
     public void testTransitionHappensCancelNavigation() {
         // Create a floating task and a fullscreen task, then navigating on fullscreen task.
         // The navigation should not been cancelled when transition happens on floating task, and
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 2e0d4d4..2f2b473 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -31,7 +31,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.testng.Assert.assertFalse;
 
@@ -357,8 +356,6 @@
 
     @Test
     public void testRemovesStaleDisplaySettings_defaultDisplay_removesStaleDisplaySettings() {
-        assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings());
-
         // Write density setting for second display then remove it.
         final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
@@ -387,8 +384,6 @@
 
     @Test
     public void testRemovesStaleDisplaySettings_displayNotInLayout_keepsDisplaySettings() {
-        assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings());
-
         // Write density setting for primary display.
         final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 61a6f31..33df5d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -300,7 +300,9 @@
         // Vertical thin letterbox disabled
         doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxHeightPx();
-        assertFalse(mController.isVerticalThinLetterboxed());
+        final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController
+                .getAppCompatReachabilityOverrides();
+        assertFalse(reachabilityOverrides.isVerticalThinLetterboxed());
         // Define a Task 100x100
         final Task task = mock(Task.class);
         doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
@@ -309,21 +311,21 @@
 
         // Vertical thin letterbox disabled without Task
         doReturn(null).when(mActivity).getTask();
-        assertFalse(mController.isVerticalThinLetterboxed());
+        assertFalse(reachabilityOverrides.isVerticalThinLetterboxed());
         // Assign a Task for the Activity
         doReturn(task).when(mActivity).getTask();
 
         // (task.width() - act.width()) / 2  = 5 < 10
         doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
-        assertTrue(mController.isVerticalThinLetterboxed());
+        assertTrue(reachabilityOverrides.isVerticalThinLetterboxed());
 
         // (task.width() - act.width()) / 2  = 10 = 10
         doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
-        assertTrue(mController.isVerticalThinLetterboxed());
+        assertTrue(reachabilityOverrides.isVerticalThinLetterboxed());
 
         // (task.width() - act.width()) / 2  = 11 > 10
         doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
-        assertFalse(mController.isVerticalThinLetterboxed());
+        assertFalse(reachabilityOverrides.isVerticalThinLetterboxed());
     }
 
     @Test
@@ -331,7 +333,9 @@
         // Horizontal thin letterbox disabled
         doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxWidthPx();
-        assertFalse(mController.isHorizontalThinLetterboxed());
+        final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController
+                .getAppCompatReachabilityOverrides();
+        assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed());
         // Define a Task 100x100
         final Task task = mock(Task.class);
         doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
@@ -340,51 +344,55 @@
 
         // Vertical thin letterbox disabled without Task
         doReturn(null).when(mActivity).getTask();
-        assertFalse(mController.isHorizontalThinLetterboxed());
+        assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed());
         // Assign a Task for the Activity
         doReturn(task).when(mActivity).getTask();
 
         // (task.height() - act.height()) / 2  = 5 < 10
         doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
-        assertTrue(mController.isHorizontalThinLetterboxed());
+        assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed());
 
         // (task.height() - act.height()) / 2  = 10 = 10
         doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
-        assertTrue(mController.isHorizontalThinLetterboxed());
+        assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed());
 
         // (task.height() - act.height()) / 2  = 11 > 10
         doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
-        assertFalse(mController.isHorizontalThinLetterboxed());
+        assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed());
     }
 
     @Test
     @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY)
     public void testAllowReachabilityForThinLetterboxWithFlagEnabled() {
-        spyOn(mController);
-        doReturn(true).when(mController).isVerticalThinLetterboxed();
-        assertFalse(mController.allowVerticalReachabilityForThinLetterbox());
-        doReturn(true).when(mController).isHorizontalThinLetterboxed();
-        assertFalse(mController.allowHorizontalReachabilityForThinLetterbox());
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+        spyOn(reachabilityOverrides);
+        doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed();
+        assertFalse(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox());
+        doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed();
+        assertFalse(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox());
 
-        doReturn(false).when(mController).isVerticalThinLetterboxed();
-        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
-        doReturn(false).when(mController).isHorizontalThinLetterboxed();
-        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+        doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox());
+        doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox());
     }
 
     @Test
     @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY)
     public void testAllowReachabilityForThinLetterboxWithFlagDisabled() {
-        spyOn(mController);
-        doReturn(true).when(mController).isVerticalThinLetterboxed();
-        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
-        doReturn(true).when(mController).isHorizontalThinLetterboxed();
-        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+        spyOn(reachabilityOverrides);
+        doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox());
+        doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox());
 
-        doReturn(false).when(mController).isVerticalThinLetterboxed();
-        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
-        doReturn(false).when(mController).isHorizontalThinLetterboxed();
-        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+        doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox());
+        doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed();
+        assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index ed93a8c..7dc3b07 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -3431,9 +3431,10 @@
         mActivity.getWindowConfiguration().setBounds(null);
 
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false);
-
-        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
-        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+        assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled());
+        assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3456,7 +3457,8 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Horizontal reachability is disabled because the app is in split screen.
-        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3479,7 +3481,8 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Vertical reachability is disabled because the app is in split screen.
-        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isVerticalReachabilityEnabled());
     }
 
     @Test
@@ -3501,7 +3504,8 @@
         // Vertical reachability is disabled because the app does not match parent width
         assertNotEquals(mActivity.getScreenResolvedBounds().width(),
                 mActivity.mDisplayContent.getBounds().width());
-        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isVerticalReachabilityEnabled());
     }
 
     @Test
@@ -3518,7 +3522,8 @@
         assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds());
 
         // Vertical reachability is still enabled as resolved bounds is not empty
-        assertTrue(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isVerticalReachabilityEnabled());
     }
 
     @Test
@@ -3535,7 +3540,8 @@
         assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds());
 
         // Horizontal reachability is still enabled as resolved bounds is not empty
-        assertTrue(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3549,7 +3555,8 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_PORTRAIT);
 
-        assertTrue(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3563,7 +3570,8 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_LANDSCAPE);
 
-        assertTrue(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isVerticalReachabilityEnabled());
     }
 
     @Test
@@ -3585,7 +3593,8 @@
         // Horizontal reachability is disabled because the app does not match parent height
         assertNotEquals(mActivity.getScreenResolvedBounds().height(),
                 mActivity.mDisplayContent.getBounds().height());
-        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3607,7 +3616,8 @@
         // Horizontal reachability is enabled because the app matches parent height
         assertEquals(mActivity.getScreenResolvedBounds().height(),
                 mActivity.mDisplayContent.getBounds().height());
-        assertTrue(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isHorizontalReachabilityEnabled());
     }
 
     @Test
@@ -3629,7 +3639,8 @@
         // Vertical reachability is enabled because the app matches parent width
         assertEquals(mActivity.getScreenResolvedBounds().width(),
                 mActivity.mDisplayContent.getBounds().width());
-        assertTrue(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+                .isVerticalReachabilityEnabled());
     }
 
     @Test
@@ -4809,10 +4820,12 @@
     }
 
     private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
-        spyOn(mActivity.mLetterboxUiController);
-        doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+        final AppCompatReachabilityOverrides reachabilityOverrides =
+                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+        spyOn(reachabilityOverrides);
+        doReturn(thinLetterboxAllowed).when(reachabilityOverrides)
                 .allowVerticalReachabilityForThinLetterbox();
-        doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+        doReturn(thinLetterboxAllowed).when(reachabilityOverrides)
                 .allowHorizontalReachabilityForThinLetterbox();
     }
 
diff --git a/telecomm/java/android/telecom/CallerInfoAsyncQuery.java b/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
index 93cd291..a03d7e2 100644
--- a/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
+++ b/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
@@ -483,18 +483,28 @@
 
         // check to see if these are recognized numbers, and use shortcuts if we can.
         TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+
         boolean isEmergencyNumber = false;
         try {
             isEmergencyNumber = tm.isEmergencyNumber(number);
-        } catch (IllegalStateException ise) {
+        } catch (IllegalStateException | UnsupportedOperationException ise) {
             // Ignore the exception that Telephony is not up. Use PhoneNumberUtils API now.
             // Ideally the PhoneNumberUtils API needs to be removed once the
             // telphony service not up issue can be fixed (b/187412989)
+            // UnsupportedOperationException: telephony.calling may not be supported on this device
             isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(context, number);
         }
+
+        boolean isVoicemailNumber;
+        try {
+            isVoicemailNumber = PhoneNumberUtils.isVoiceMailNumber(context, subId, number);
+        } catch (UnsupportedOperationException ex) {
+            isVoicemailNumber = false;
+        }
+
         if (isEmergencyNumber) {
             cw.event = EVENT_EMERGENCY_NUMBER;
-        } else if (PhoneNumberUtils.isVoiceMailNumber(context, subId, number)) {
+        } else if (isVoicemailNumber) {
             cw.event = EVENT_VOICEMAIL_NUMBER;
         } else {
             cw.event = EVENT_NEW_QUERY;
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 94d4d22..9b83719 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -528,7 +528,7 @@
                 // current SMS app will already be the preferred activity - but checking whether or
                 // not this is true is just as expensive as reconfiguring the preferred activity so
                 // we just reconfigure every time.
-                defaultSmsAppChanged(context);
+                grantPermissionsToSystemApps(context);
             }
         }
         if (DEBUG_MULTIUSER) {
@@ -542,9 +542,9 @@
     }
 
     /**
-     * Grants various permissions and appops on sms app change
+     * Grants various permissions and appops, e.g. on sms app change
      */
-    private static void defaultSmsAppChanged(Context context) {
+    public static void grantPermissionsToSystemApps(Context context) {
         PackageManager packageManager = context.getPackageManager();
         AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
 
@@ -680,7 +680,7 @@
                 return;
             }
 
-            defaultSmsAppChanged(context);
+            grantPermissionsToSystemApps(context);
         }
     }
 
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0468f48..b9a001d 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -51,6 +51,7 @@
 import android.telephony.ims.RcsUceAdapter;
 import android.telephony.ims.feature.MmTelFeature;
 import android.telephony.ims.feature.RcsFeature;
+import android.telephony.satellite.SatelliteManager;
 
 import com.android.internal.telephony.ICarrierConfigLoader;
 import com.android.internal.telephony.flags.Flags;
@@ -9603,9 +9604,8 @@
      * Defines the rules for data setup retry.
      *
      * The syntax of the retry rule:
-     * 1. Retry based on {@link NetworkCapabilities}. Note that only APN-type network capabilities
-     *    are supported. If the capabilities are not specified, then the retry rule only applies
-     *    to the current failed APN used in setup data call request.
+     * 1. Retry based on {@link NetworkCapabilities}. If the capabilities are not specified, then
+     * the retry rule only applies to the current failed APN used in setup data call request.
      * "capabilities=[netCaps1|netCaps2|...], [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]"
      *
      * 2. Retry based on {@link DataFailCause}
@@ -10022,6 +10022,22 @@
             "carrier_roaming_ntn_connect_type_int";
 
     /**
+     * Indicates carrier roaming non-terrestrial network emergency call handover type that the
+     * device will use to perform a handover between ESOS or T911.
+     * If this key is set to {@link SatelliteManager#EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_SOS}
+     * then the handover will be made to ESOS. If this key is set to
+     * {@link SatelliteManager#EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911} then the handover
+     * will be made to T911.
+     *
+     * The default value is {@link SatelliteManager#EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911}.
+     *
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static final String
+            KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT =
+            "carrier_roaming_ntn_emergency_call_to_satellite_handover_type_int";
+
+    /**
      * The carrier roaming non-terrestrial network hysteresis time in seconds.
      *
      * If the device supports P2P satellite messaging which is defined by
@@ -10038,6 +10054,19 @@
             "carrier_supported_satellite_notification_hysteresis_sec_int";
 
     /**
+     * An integer key holds the timeout duration in seconds used to determine whether to exit
+     * carrier-roaming NB-IOT satellite mode.
+     *
+     * The timer is started when the device screen is turned off during a satellite session.
+     * When the timer expires, the device exits Carrier Roaming NB IOT NTN.
+     *
+     * The default value is 30 seconds.
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static final String KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT =
+            "satellite_screen_off_inactivity_timeout_duration_sec_int";
+
+    /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
      * the default APN (i.e. internet) will be used for tethering.
      *
@@ -11196,7 +11225,10 @@
                 (int) TimeUnit.SECONDS.toMillis(30));
         sDefaults.putBoolean(KEY_SATELLITE_ESOS_SUPPORTED_BOOL, false);
         sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, 0);
+        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);
+        sDefaults.putInt(KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
         sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
index 9ff73e2..66a20ae 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
@@ -27,4 +27,11 @@
      * @param state The current satellite modem state.
      */
     void onSatelliteModemStateChanged(in int state);
+
+    /**
+     * Indicates that the satellite emergency mode has changed.
+     *
+     * @param isEmergency True means satellite enabled for emergency mode, false otherwise.
+     */
+    void onEmergencyModeChanged(in boolean isEmergency);
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 4b83b65..0bd9270 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1531,6 +1531,12 @@
                         executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                 callback.onSatelliteModemStateChanged(state)));
                     }
+
+                    @Override
+                    public void onEmergencyModeChanged(boolean isEmergency) {
+                        executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                callback.onEmergencyModeChanged(isEmergency)));
+                    }
                 };
                 sSatelliteModemStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForSatelliteModemStateChanged(mSubId, internalCallback);
diff --git a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
index 8d33c88..423a785 100644
--- a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
@@ -35,4 +35,14 @@
      */
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     void onSatelliteModemStateChanged(@SatelliteManager.SatelliteModemState int state);
+
+    /**
+     * Called when the satellite emergency mode has changed.
+     *
+     * @param isEmergency {@code true} enabled for emergency mode, {@code false} otherwise.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    default void onEmergencyModeChanged(boolean isEmergency) {};
 }
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 45bf8e3..9444dd9 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -107,7 +107,7 @@
   Visibility::Level visibility_level = Visibility::Level::kUndefined;
   bool staged_api = false;
   bool allow_new = false;
-  FlagStatus flag_status;
+  FlagStatus flag_status = FlagStatus::NoFlag;
   std::optional<OverlayableItem> overlayable_item;
   std::optional<StagedId> staged_alias;
 
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 9530c17..4f76e7d 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,7 +104,7 @@
   // The actual Value.
   std::unique_ptr<Value> value;
 
-  FlagStatus flag_status;
+  FlagStatus flag_status = FlagStatus::NoFlag;
 
   ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
       : config(config), product(product) {
@@ -271,7 +271,7 @@
   std::optional<AllowNew> allow_new;
   std::optional<StagedId> staged_id;
   bool allow_mangled = false;
-  FlagStatus flag_status;
+  FlagStatus flag_status = FlagStatus::NoFlag;
 };
 
 struct NewResourceBuilder {
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 67a4828..1942fc11 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -212,7 +212,11 @@
     collision_result =
         ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool);
   } else {
-    collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value);
+    collision_result = ResourceTable::ResolveFlagCollision(dst_config_value->flag_status,
+                                                           src_config_value->flag_status);
+    if (collision_result == CollisionResult::kConflict) {
+      collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value);
+    }
   }
 
   if (collision_result == CollisionResult::kConflict) {
@@ -291,6 +295,7 @@
         } else {
           dst_config_value =
               dst_entry->FindOrCreateValue(src_config_value->config, src_config_value->product);
+          dst_config_value->flag_status = src_config_value->flag_status;
         }
 
         // Continue if we're taking the new resource.
diff --git a/wifi/java/src/android/net/wifi/WifiMigration.java b/wifi/java/src/android/net/wifi/WifiMigration.java
index 1a20a12..6ea20ec 100644
--- a/wifi/java/src/android/net/wifi/WifiMigration.java
+++ b/wifi/java/src/android/net/wifi/WifiMigration.java
@@ -574,7 +574,7 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_LEGACY_KEYSTORE_TO_WIFI_BLOBSTORE_MIGRATION)
+    @FlaggedApi(Flags.FLAG_LEGACY_KEYSTORE_TO_WIFI_BLOBSTORE_MIGRATION_READ_ONLY)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static void migrateLegacyKeystoreToWifiBlobstore() {
         final long identity = Binder.clearCallingIdentity();
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index 5a214b7..90d13e6 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -19,11 +19,12 @@
 }
 
 flag {
-    name: "legacy_keystore_to_wifi_blobstore_migration"
+    name: "legacy_keystore_to_wifi_blobstore_migration_read_only"
     is_exported: true
     namespace: "wifi"
     description: "Add API to migrate all values from Legacy Keystore to the new Wifi Blobstore database"
     bug: "332560152"
+    is_fixed_read_only: true
 }
 
 flag {